upd
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
function Badge({ count }: { count: number }) {
|
||||
return (
|
||||
<div className='w-[1.389vw] h-[1.389vw] rounded-full bg-[#F8F7FE] caption-xs text-[#7B60F3] flex justify-center items-center font-medium'>
|
||||
<div className="size-[1.389vw] rounded-full bg-[#F8F7FE] caption-xs text-[#7B60F3] flex justify-center items-center font-medium font-mono">
|
||||
{count}
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -2,7 +2,7 @@ import clsx from "clsx";
|
||||
|
||||
interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
|
||||
children: React.ReactNode;
|
||||
variant: "critical" | "secondary" | "primary" | "cta" | "menu";
|
||||
variant?: "critical" | "secondary" | "primary" | "cta" | "menu";
|
||||
className?: string;
|
||||
size?: "small" | "medium" | "large";
|
||||
ref?: React.RefObject<HTMLButtonElement | null>;
|
||||
@@ -10,7 +10,7 @@ interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
|
||||
|
||||
function Button({
|
||||
children,
|
||||
variant = "primary",
|
||||
variant = "cta",
|
||||
size = "medium",
|
||||
className,
|
||||
ref,
|
||||
@@ -27,7 +27,7 @@ function Button({
|
||||
onClick?.(e);
|
||||
}}
|
||||
className={clsx(
|
||||
"transition-all flex 2xl:gap-[0.556vw] gap-2 items-center justify-center font-medium disabled:bg-[#F6F6F6] disabled:text-[#D6D6D6]",
|
||||
"transition-all flex outline-none 2xl:gap-[0.556vw] gap-2 items-center justify-center font-medium disabled:bg-[#F6F6F6] disabled:text-[#D6D6D6] cursor-pointerdisabled:cursor-default",
|
||||
variant === "critical" &&
|
||||
"text-[#FF4517] bg-[#FEF3F2] hover:bg-[#FEE4E2]",
|
||||
variant === "secondary" &&
|
||||
|
||||
@@ -48,7 +48,7 @@ export default function DesktopCard({ server }: IDesktopCardProps) {
|
||||
<p className="leading-none font-[500] text-[1.111vw] title-s">
|
||||
{server.name}
|
||||
</p>
|
||||
<p className="caption-s text-[#7D7D7D]">{server.location}</p>
|
||||
<p className="caption-s text-[#7D7D7D]">{server.description}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex gap-[0.278vw] justify-center -mt-[0.278vw]">
|
||||
|
||||
@@ -1,35 +0,0 @@
|
||||
import { useQueryClient } from "@tanstack/react-query";
|
||||
import ChevronDownIcon from "./icons/ChevronDownIcon";
|
||||
import { IUser } from "../types/User";
|
||||
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;
|
||||
@@ -1,18 +1,44 @@
|
||||
import React from "react";
|
||||
import clsx from "clsx";
|
||||
|
||||
interface InputProps extends React.InputHTMLAttributes<HTMLInputElement> {
|
||||
label?: string;
|
||||
interface NewInputProps extends React.InputHTMLAttributes<HTMLInputElement> {
|
||||
placeholder?: string;
|
||||
isError?: boolean;
|
||||
errorMessage?: string;
|
||||
}
|
||||
|
||||
function Input({ label, ...props }: InputProps) {
|
||||
function Input({
|
||||
placeholder,
|
||||
isError,
|
||||
errorMessage,
|
||||
...props
|
||||
}: NewInputProps) {
|
||||
return (
|
||||
<label className="space-y-2">
|
||||
{label && <p className="text-xs text-black/50">{label}</p>}
|
||||
<div className="relative">
|
||||
<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"
|
||||
placeholder=""
|
||||
className={clsx(
|
||||
isError
|
||||
? "hover:ring-[#FF4517] focus:ring-[#FF4517]"
|
||||
: "hover:ring-[#7B60F3] focus:ring-[#7B60F3]",
|
||||
"peer bg-[#F6F6F6] rounded-[0.833vw] px-[1.111vw] pt-[19px] pb-[11px] outline-none ring-1 ring-transparent transition-all inline-block w-full h-[3.889vw] text-m"
|
||||
)}
|
||||
/>
|
||||
</label>
|
||||
{placeholder && (
|
||||
<span
|
||||
className="absolute caption-m font-medium text-[#7D7D7D] left-[1.111vw] top-1/2 -translate-y-1/2 pointer-events-none transition-all duration-300
|
||||
peer-focus:caption-xs peer-focus:top-[0.556vw] peer-focus:translate-y-0
|
||||
peer-[:not(:placeholder-shown)]:caption-xs peer-[:not(:placeholder-shown)]:top-[0.556vw] peer-[:not(:placeholder-shown)]:translate-y-0"
|
||||
>
|
||||
{placeholder}
|
||||
</span>
|
||||
)}
|
||||
{isError && (
|
||||
<p className="caption-s font-medium text-[#FF4517] mt-[0.556vw]">
|
||||
{errorMessage}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,155 @@
|
||||
/* eslint-disable react-hooks/exhaustive-deps */
|
||||
import { useEffect, useState } from "react";
|
||||
import ChevronDownIcon from "./icons/ChevronDownIcon";
|
||||
import ChevronUpIcon from "./icons/ChevronUpIcon";
|
||||
import clsx from "clsx";
|
||||
import CloseIcon from "./icons/CloseIcon";
|
||||
import Button from "./Button";
|
||||
import CheckIcon from "./icons/CheckIcon";
|
||||
import { useClickAway } from "@uidotdev/usehooks";
|
||||
import { AnimatePresence, motion } from "motion/react";
|
||||
|
||||
function MultySelect<T extends { name: string; id: string }>({
|
||||
data,
|
||||
isGrid,
|
||||
placeholder,
|
||||
resetTitle,
|
||||
onSelect,
|
||||
}: {
|
||||
data: T[];
|
||||
isGrid: boolean;
|
||||
placeholder: string;
|
||||
resetTitle: string;
|
||||
onSelect: (values: T[]) => void;
|
||||
}) {
|
||||
const [selectedValues, setSelectedValues] = useState<T[]>([]);
|
||||
const [isSelectVisible, setIsSelectVisible] = useState(false);
|
||||
|
||||
const selectRef = useClickAway<HTMLDivElement>(() => {
|
||||
setIsSelectVisible(false);
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
onSelect(selectedValues);
|
||||
}, [selectedValues]);
|
||||
|
||||
const handleSelectClick = (item: T) => {
|
||||
const isItemSelected = selectedValues.some((val) => val.id === item.id);
|
||||
|
||||
if (isItemSelected) {
|
||||
setSelectedValues(selectedValues.filter((value) => value.id !== item.id));
|
||||
} else {
|
||||
setSelectedValues([...selectedValues, item]);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className="relative w-[19.583vw] bg-[#F6F6F6] rounded-[0.833vw] select-none"
|
||||
ref={selectRef}
|
||||
>
|
||||
<div
|
||||
className={clsx(
|
||||
"flex items-center justify-between px-[1.111vw] py-[1.285vw] hover:bg-[#F0F0F0] rounded-[0.833vw] cursor-pointer",
|
||||
isSelectVisible
|
||||
? "!bg-[#E1DEFC] !text-[#7B60F3] hover:bg-[#E1DEFC]"
|
||||
: "text-[#141414]"
|
||||
)}
|
||||
onClick={() => setIsSelectVisible(!isSelectVisible)}
|
||||
>
|
||||
<div className="button-m font-medium text-ellipsis line-clamp-1 flex-1">
|
||||
{selectedValues.length > 0
|
||||
? selectedValues.map(({ name }) => name).join(", ")
|
||||
: placeholder}
|
||||
</div>
|
||||
<span
|
||||
className={clsx(
|
||||
"w-[1.389vw] h-[1.389vw] flex items-center justify-center",
|
||||
isSelectVisible ? "text-[#7B60F3]" : "text-[#7D7D7D]"
|
||||
)}
|
||||
>
|
||||
{isSelectVisible ? <ChevronUpIcon /> : <ChevronDownIcon />}
|
||||
</span>
|
||||
</div>
|
||||
<AnimatePresence>
|
||||
{isSelectVisible && (
|
||||
<motion.div
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
exit={{ opacity: 0 }}
|
||||
className="absolute top-[calc(100%+0.278vw)] flex flex-col gap-[0.278vw] rounded-[0.833vw] w-full bg-white z-1"
|
||||
>
|
||||
<div
|
||||
className="flex flex-col gap-[0.278vw] px-[0.833vw] pb-[0.833vw] w-full bg-white rounded-[0.833vw] max-h-[13.889vw] overflow-auto"
|
||||
style={{
|
||||
boxShadow:
|
||||
"0px 4px 40px 0px #0000000D, 0px 2px 2px 0px #0000000D",
|
||||
}}
|
||||
>
|
||||
<div className="bg-white sticky top-0 pt-[0.833vw] z-1">
|
||||
<Button
|
||||
variant="secondary"
|
||||
className="!justify-start w-full text-s font-medium p-[0.833vw] text-[#7D7D7D] flex items-center gap-[0.278vw] cursor-pointer rounded-[0.278vw] hover:bg-[#F6F6F6] bg-white"
|
||||
onClick={() => setSelectedValues([])}
|
||||
>
|
||||
<span className="size-[1.111vw] flex items-center justify-center">
|
||||
<CloseIcon />
|
||||
</span>
|
||||
{resetTitle}
|
||||
</Button>
|
||||
</div>
|
||||
<div className="min-h-[1px] w-full bg-[#F6F6F6]" />
|
||||
{isGrid ? (
|
||||
<div className="grid grid-cols-3 gap-[0.139vw]">
|
||||
{data.map((item) => (
|
||||
<div
|
||||
key={item.id}
|
||||
className="p-[0.556vw] flex flex-col gap-[0.278vw] justify-center items-center cursor-pointer rounded-[0.278vw] hover:bg-[#F6F6F6]"
|
||||
onClick={() => handleSelectClick(item)}
|
||||
>
|
||||
<div className="relative size-[2.222vw] rounded-full bg-[#d4d4d4]">
|
||||
{selectedValues
|
||||
.map((value) => value.id)
|
||||
.includes(item.id) && (
|
||||
<span className="absolute bottom-[1.389vw] left-[1.389vw] rounded-full size-[1.111vw] bg-[#7B60F3] text-white flex items-center justify-center">
|
||||
<div className="size-[0.833vw]">
|
||||
<CheckIcon />
|
||||
</div>
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<p className="text-s font-medium">{item.name}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
{data.map((item) => (
|
||||
<div
|
||||
key={item.id}
|
||||
className="flex gap-[0.278vw] w-full p-[0.833vw] text-s rounded-[0.278vw] hover:bg-[#F6F6F6] cursor-pointer"
|
||||
onClick={() => handleSelectClick(item)}
|
||||
>
|
||||
<div className="size-[1.111vw]">
|
||||
{selectedValues
|
||||
.map((value) => value.id)
|
||||
.includes(item.id) && (
|
||||
<span className="text-[#7B60F3]">
|
||||
<CheckIcon />
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
{item.name}
|
||||
</div>
|
||||
))}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default MultySelect;
|
||||
@@ -10,7 +10,7 @@ import AppsIcon from "./icons/AppsIcon";
|
||||
function Navbar() {
|
||||
return (
|
||||
<div className="flex 2xl:gap-[0.278vw] items-center">
|
||||
<NavLink to="/">
|
||||
<NavLink to="/" className="outline-none">
|
||||
{({ isActive }) => (
|
||||
<Button
|
||||
variant="menu"
|
||||
@@ -26,7 +26,7 @@ function Navbar() {
|
||||
</Button>
|
||||
)}
|
||||
</NavLink>
|
||||
<NavLink to="/sessions">
|
||||
<NavLink to="/sessions" className="outline-none">
|
||||
{({ isActive }) => (
|
||||
<Button
|
||||
variant="menu"
|
||||
@@ -42,7 +42,7 @@ function Navbar() {
|
||||
</Button>
|
||||
)}
|
||||
</NavLink>
|
||||
<NavLink to="/managers">
|
||||
<NavLink to="/managers" className="outline-none">
|
||||
{({ isActive }) => (
|
||||
<Button
|
||||
variant="menu"
|
||||
@@ -58,7 +58,7 @@ function Navbar() {
|
||||
</Button>
|
||||
)}
|
||||
</NavLink>
|
||||
<NavLink to="/clients">
|
||||
<NavLink to="/clients" className="outline-none">
|
||||
{({ isActive }) => (
|
||||
<Button
|
||||
variant="menu"
|
||||
@@ -74,7 +74,7 @@ function Navbar() {
|
||||
</Button>
|
||||
)}
|
||||
</NavLink>
|
||||
<NavLink to="/projects">
|
||||
<NavLink to="/projects" className="outline-none">
|
||||
{({ isActive }) => (
|
||||
<Button
|
||||
variant="menu"
|
||||
|
||||
@@ -1,45 +0,0 @@
|
||||
import clsx from "clsx";
|
||||
|
||||
interface NewInputProps extends React.InputHTMLAttributes<HTMLInputElement> {
|
||||
placeholder?: string;
|
||||
isError?: boolean;
|
||||
errorMessage?: string;
|
||||
}
|
||||
|
||||
function NewInput({
|
||||
placeholder,
|
||||
isError,
|
||||
errorMessage,
|
||||
...props
|
||||
}: NewInputProps) {
|
||||
return (
|
||||
<div className="relative">
|
||||
<input
|
||||
{...props}
|
||||
placeholder=""
|
||||
className={clsx(
|
||||
isError
|
||||
? "hover:ring-[#FF4517] focus:ring-[#FF4517]"
|
||||
: "hover:ring-[#7B60F3] focus:ring-[#7B60F3]",
|
||||
"peer bg-[#F6F6F6] rounded-[0.833vw] px-[1.111vw] pt-[19px] pb-[11px] outline-none ring-1 ring-transparent transition-all inline-block w-full h-[3.889vw] text-m"
|
||||
)}
|
||||
/>
|
||||
{placeholder && (
|
||||
<span
|
||||
className="absolute caption-m font-medium text-[#7D7D7D] left-[1.111vw] top-1/2 -translate-y-1/2 pointer-events-none transition-all duration-300
|
||||
peer-focus:caption-xs peer-focus:top-[0.556vw] peer-focus:translate-y-0
|
||||
peer-[:not(:placeholder-shown)]:caption-xs peer-[:not(:placeholder-shown)]:top-[0.556vw] peer-[:not(:placeholder-shown)]:translate-y-0"
|
||||
>
|
||||
{placeholder}
|
||||
</span>
|
||||
)}
|
||||
{isError && (
|
||||
<p className="caption-s font-medium text-[#FF4517] mt-[0.556vw]">
|
||||
{errorMessage}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default NewInput;
|
||||
@@ -1,147 +0,0 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import ChevronDownIcon from "./icons/ChevronDownIcon";
|
||||
import ChevronUpIcon from "./icons/ChevronUpIcon";
|
||||
import clsx from "clsx";
|
||||
import CloseIcon from "./icons/CloseIcon";
|
||||
import Button from "./Button";
|
||||
import CheckIcon from "./icons/CheckIcon";
|
||||
import { useClickAway } from "@uidotdev/usehooks";
|
||||
|
||||
function NewSelect<T extends { name: string }>({
|
||||
data,
|
||||
isGrid,
|
||||
placeholder,
|
||||
resetTitle,
|
||||
}: {
|
||||
data: T[];
|
||||
isGrid: boolean;
|
||||
placeholder: string;
|
||||
resetTitle: string;
|
||||
}) {
|
||||
const [selectedValues, setSelectedValues] = useState<T[]>([]);
|
||||
const [isSelectVisible, setIsSelectVisible] = useState(false);
|
||||
const selectRef = useClickAway<HTMLDivElement>(() => {
|
||||
setIsSelectVisible(false);
|
||||
});
|
||||
|
||||
const handleSelectClick = (item: T) => {
|
||||
const isItemSelected = selectedValues.some((val) => val.name === item.name);
|
||||
|
||||
if (isItemSelected) {
|
||||
setSelectedValues(
|
||||
selectedValues.filter((value) => value.name !== item.name)
|
||||
);
|
||||
} else {
|
||||
setSelectedValues([...selectedValues, item]);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
console.log(selectedValues);
|
||||
}, [selectedValues]);
|
||||
|
||||
return (
|
||||
<div
|
||||
className="relative w-[19.583vw] bg-white rounded-[0.833vw] select-none"
|
||||
ref={selectRef}
|
||||
>
|
||||
<div
|
||||
className={clsx(
|
||||
"flex items-center justify-between px-[1.111vw] py-[1.285vw] hover:bg-[#F0F0F0] rounded-[0.833vw] cursor-pointer",
|
||||
isSelectVisible
|
||||
? "!bg-[#E1DEFC] !text-[#7B60F3] hover:bg-[#E1DEFC]"
|
||||
: "text-[#141414]"
|
||||
)}
|
||||
onClick={() => setIsSelectVisible(!isSelectVisible)}
|
||||
>
|
||||
<div className="button-m font-medium text-ellipsis line-clamp-1 flex-1">
|
||||
{selectedValues.length > 0
|
||||
? selectedValues.map(({ name }) => name).join(", ")
|
||||
: placeholder}
|
||||
</div>
|
||||
<span
|
||||
className={clsx(
|
||||
"w-[1.389vw] h-[1.389vw] flex items-center justify-center",
|
||||
isSelectVisible ? "text-[#7B60F3]" : "text-[#7D7D7D]"
|
||||
)}
|
||||
>
|
||||
{isSelectVisible ? <ChevronUpIcon /> : <ChevronDownIcon />}
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
className={clsx(
|
||||
"absolute top-[calc(100%+0.278vw)] flex flex-col gap-[0.278vw] rounded-[0.833vw] w-full",
|
||||
isSelectVisible ? "block" : "hidden"
|
||||
)}
|
||||
>
|
||||
<div
|
||||
className="flex flex-col gap-[0.278vw] px-[0.833vw] pb-[0.833vw] w-full bg-white rounded-[0.833vw] max-h-[13.889vw] overflow-auto"
|
||||
style={{
|
||||
boxShadow: "0px 4px 40px 0px #0000000D, 0px 2px 2px 0px #0000000D",
|
||||
}}
|
||||
>
|
||||
<div className="bg-white sticky top-0 pt-[0.833vw] z-1">
|
||||
<Button
|
||||
variant="secondary"
|
||||
className="!justify-start w-full text-s font-medium p-[0.833vw] text-[#7D7D7D] flex items-center gap-[0.278vw] cursor-pointer rounded-[0.278vw] hover:bg-[#F6F6F6] bg-white"
|
||||
onClick={() => setSelectedValues([])}
|
||||
>
|
||||
<span className="size-[1.111vw] flex items-center justify-center">
|
||||
<CloseIcon />
|
||||
</span>
|
||||
{resetTitle}
|
||||
</Button>
|
||||
</div>
|
||||
<div className="min-h-[1px] w-full bg-[#F6F6F6]" />
|
||||
{isGrid ? (
|
||||
<div className="grid grid-cols-3 gap-[0.139vw]">
|
||||
{data.map((item) => (
|
||||
<div
|
||||
key={item.name}
|
||||
className=" p-[0.556vw] flex flex-col gap-[0.278vw] justify-center items-center cursor-pointer rounded-[0.278vw] hover:bg-[#F6F6F6]"
|
||||
onClick={() => handleSelectClick(item)}
|
||||
>
|
||||
<div className="relative size-[2.222vw] rounded-full bg-[#d4d4d4]">
|
||||
{selectedValues
|
||||
.map((value) => value.name)
|
||||
.includes(item.name) && (
|
||||
<span className="absolute bottom-[1.389vw] left-[1.389vw] rounded-full size-[1.111vw] bg-[#7B60F3] text-white flex items-center justify-center">
|
||||
<div className="size-[0.833vw]">
|
||||
<CheckIcon />
|
||||
</div>
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<p className="text-s font-medium">{item.name}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
{data.map((item, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className="flex gap-[0.278vw] w-full p-[0.833vw] text-s rounded-[0.278vw] hover:bg-[#F6F6F6] cursor-pointer"
|
||||
onClick={() => handleSelectClick(item)}
|
||||
>
|
||||
<div className="size-[1.111vw]">
|
||||
{selectedValues
|
||||
.map((value) => value.name)
|
||||
.includes(item.name) && (
|
||||
<span className="text-[#7B60F3]">
|
||||
<CheckIcon />
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
{item.name}
|
||||
</div>
|
||||
))}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default NewSelect;
|
||||
@@ -1,23 +1,62 @@
|
||||
import React, { useState } from "react";
|
||||
import React, { useRef } from "react";
|
||||
import SearchIcon from "./icons/SearchIcon";
|
||||
import CloseIcon from "./icons/CloseIcon";
|
||||
import Button from "./Button";
|
||||
import clsx from "clsx";
|
||||
|
||||
function SearchInput(props: React.InputHTMLAttributes<HTMLInputElement>) {
|
||||
const [value, setValue] = useState("");
|
||||
function SearchInput(
|
||||
props: React.InputHTMLAttributes<HTMLInputElement> & {
|
||||
onEnter?: () => void;
|
||||
}
|
||||
) {
|
||||
const ref = useRef<HTMLInputElement>(null);
|
||||
|
||||
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
|
||||
className={clsx(
|
||||
"p-[0.556vw] bg-[#F6F6F6] rounded-[0.833vw] w-full flex items-center gap-[1.111vw] hover:bg-[#F0F0F0]",
|
||||
!props.onEnter && "px-[1.111vw] py-[1.215vw]"
|
||||
)}
|
||||
>
|
||||
<div className="flex gap-[0.566vw] items-center flex-1">
|
||||
<span className="text-[#7D7D7D] size-[1.389vw]">
|
||||
<SearchIcon />
|
||||
</span>
|
||||
<input
|
||||
className="outline-none focus:outline-none placeholder:button-m placeholder:font-medium placeholder:text-[#7D7D7D] button-m font-medium flex-1"
|
||||
{...props}
|
||||
ref={ref}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter") props.onEnter?.();
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
className={clsx(
|
||||
"flex gap-[0.566vw] items-center transition-opacity",
|
||||
!props.value && "opacity-0"
|
||||
)}
|
||||
>
|
||||
<button
|
||||
disabled={!props.value}
|
||||
className="outline-none cursor-pointer disabled:cursor-default"
|
||||
// onClick={() => {
|
||||
// if (ref.current) {
|
||||
// ref.current.value = "";
|
||||
// ref.current.focus();
|
||||
// }
|
||||
// }}
|
||||
>
|
||||
<div className="text-[#7D7D7D] size-[1.111vw]">
|
||||
<CloseIcon />
|
||||
</div>
|
||||
</button>
|
||||
{props.onEnter && (
|
||||
<Button size="small" disabled={!props.value} onClick={props.onEnter}>
|
||||
Искать
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -80,7 +80,7 @@ function SessionComments({ sessionId }: { sessionId: string }) {
|
||||
};
|
||||
|
||||
const handleKeyDown = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
|
||||
if (e.key === "Enter") {
|
||||
if (e.key === "Enter" && !e.shiftKey) {
|
||||
e.preventDefault();
|
||||
handleSubmit(e as unknown as React.FormEvent<HTMLFormElement>);
|
||||
}
|
||||
@@ -115,11 +115,13 @@ function SessionComments({ sessionId }: { sessionId: string }) {
|
||||
<form
|
||||
ref={formRef}
|
||||
className="flex gap-[2.222vw] max-h-[27.778vw] bg-white justify-center items-start p-[1.111vw]"
|
||||
name="commment"
|
||||
onSubmit={handleSubmit}
|
||||
>
|
||||
<textarea
|
||||
ref={textareaRef}
|
||||
rows={1}
|
||||
name="comment"
|
||||
className="w-[17.083vw] outline-none text-s resize-none self-center"
|
||||
placeholder="Расскажите, как все прошло"
|
||||
style={{
|
||||
|
||||
@@ -41,9 +41,9 @@ export function SessionFile({
|
||||
className="flex gap-[0.833vw] items-center px-[0.833vw] py-[0.556vw]"
|
||||
>
|
||||
<div className="p-[0.972vw] bg-[#F6F6F6] rounded-[0.556vw]">
|
||||
<span className="size-[1.389vw] text-[#7D7D7D]">
|
||||
<div className="size-[1.389vw] text-[#7D7D7D]">
|
||||
<FilledHomeIcon />
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-[0.278vw] flex-1">
|
||||
<p className="button-m font-medium">{filename}</p>
|
||||
|
||||
@@ -18,7 +18,12 @@ function TableSelector({
|
||||
{tables.map((table) => (
|
||||
<button
|
||||
key={table.id}
|
||||
disabled={table.status === "offline"}
|
||||
disabled={
|
||||
table.status === "offline" ||
|
||||
(!!table.sessions &&
|
||||
table.sessions.length > 0 &&
|
||||
table.sessions[0].status !== "ended")
|
||||
}
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
onSelect(table);
|
||||
|
||||
@@ -1,16 +1,18 @@
|
||||
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"
|
||||
<svg viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<circle
|
||||
cx={9.584}
|
||||
cy={9.583}
|
||||
r={4.817}
|
||||
stroke="currentColor"
|
||||
strokeWidth={1.2}
|
||||
/>
|
||||
<path
|
||||
d="m15 15.834-2.5-2.5"
|
||||
stroke="currentColor"
|
||||
strokeWidth={1.2}
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
|
||||
@@ -6,7 +6,7 @@ import { Session } from "../../types/Session.ts";
|
||||
import { Client } from "../../types/Client.ts";
|
||||
import useModalStore from "../../stores/useModalStore.ts";
|
||||
import TableSelector from "../TableSelector.tsx";
|
||||
import NewInput from "../NewInput.tsx";
|
||||
import Input from "../Input.tsx";
|
||||
import StartSessionIcon from "../icons/StartSessionIcon.tsx";
|
||||
import Button from "../Button.tsx";
|
||||
import ProjectSelector from "../ProjectSelector.tsx";
|
||||
@@ -173,19 +173,19 @@ export default function CreateSessionModal({ targetServerId }: Props) {
|
||||
<div className="flex flex-col gap-y-[0.833vw]">
|
||||
<p className="title-s font-medium">Укажите данные клиента</p>
|
||||
<div className="flex flex-col gap-y-[0.556vw]">
|
||||
<NewInput
|
||||
<Input
|
||||
value={phone || ""}
|
||||
onChange={(e) => setPhone(e.target.value)}
|
||||
placeholder="Номер телефона"
|
||||
required
|
||||
/>
|
||||
<NewInput
|
||||
<Input
|
||||
value={name || ""}
|
||||
onChange={(e) => setName(e.target.value)}
|
||||
placeholder="Имя"
|
||||
required
|
||||
/>
|
||||
<NewInput
|
||||
<Input
|
||||
type="email"
|
||||
value={email || ""}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
|
||||
@@ -75,7 +75,7 @@ function CurrentSessionModal({ session }: { session: Session }) {
|
||||
</div>
|
||||
<div>
|
||||
<p className="caption-s font-medium text-[#BDBDBD]">
|
||||
{session.server.location}
|
||||
{session.server.description}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useState } from "react";
|
||||
import NewInput from "../NewInput";
|
||||
import Input from "../Input";
|
||||
import Button from "../Button";
|
||||
import useModalStore from "../../stores/useModalStore";
|
||||
import { Server } from "../../types/Server";
|
||||
@@ -9,7 +9,7 @@ import api from "../../utils/api";
|
||||
|
||||
function EditTable({ table }: { table: Server }) {
|
||||
const [tableName, setTableName] = useState(table.name);
|
||||
const [tableDescription, setTableDescription] = useState(table.location);
|
||||
const [tableDescription, setTableDescription] = useState(table.description);
|
||||
const { setModal } = useModalStore();
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
@@ -34,7 +34,7 @@ function EditTable({ table }: { table: Server }) {
|
||||
<div className="bg-[url(/images/Table.png)] bg-no-repeat bg-contain bg-center w-full h-[6.944vw]"></div>
|
||||
<div className="bg-[#FFFFFF] w-full rounded-[2.222vw] p-[1.389vw] flex flex-col gap-[0.833vw]">
|
||||
<div className="space-y-[0.556vw]">
|
||||
<NewInput
|
||||
<Input
|
||||
placeholder="Название стола*"
|
||||
value={tableName}
|
||||
onChange={(e) => setTableName(e.target.value)}
|
||||
@@ -48,7 +48,7 @@ function EditTable({ table }: { table: Server }) {
|
||||
)}
|
||||
</div>
|
||||
<div className="space-y-[0.556vw]">
|
||||
<NewInput
|
||||
<Input
|
||||
placeholder="Описание"
|
||||
value={tableDescription}
|
||||
onChange={(e) => setTableDescription(e.target.value)}
|
||||
|
||||
@@ -12,6 +12,8 @@ import { useQuery } from "@tanstack/react-query";
|
||||
import api from "../../utils/api";
|
||||
import { useEffect } from "react";
|
||||
import SessionFiles from "../SessionFiles";
|
||||
import DownloadIcon from "../icons/DownloadIcon";
|
||||
import ShareIcon from "../icons/ShareIcon";
|
||||
|
||||
function SessionModal({ session }: { session: Session }) {
|
||||
const { data } = useQuery({
|
||||
@@ -23,7 +25,7 @@ function SessionModal({ session }: { session: Session }) {
|
||||
sessionId: session.id,
|
||||
},
|
||||
})
|
||||
.json<string[]>(),
|
||||
.json<{ filename: string; size: number }[]>(),
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
@@ -91,7 +93,7 @@ function SessionModal({ session }: { session: Session }) {
|
||||
Речевая
|
||||
<span className="text-[#7B60F3] flex">
|
||||
аналитика
|
||||
<span className="w-[1.389vw] h-[1.389vw] text-[#7B60F3]">
|
||||
<span className="size-[1.389vw] text-[#7B60F3]">
|
||||
<MagicIcon />
|
||||
</span>
|
||||
</span>
|
||||
@@ -120,30 +122,33 @@ function SessionModal({ session }: { session: Session }) {
|
||||
</div>
|
||||
<Button variant="primary" size="large">
|
||||
Весь отчет по встрече
|
||||
<span className="w-[1.111vw] h-[1.111vw] text-[#7B60F3]">
|
||||
<span className="size-[1.111vw] text-[#7B60F3]">
|
||||
<ChevronRightIcon />
|
||||
</span>
|
||||
</Button>
|
||||
</div>
|
||||
<div className="flex flex-col gap-[1.111vw] bg-white rounded-[1.667vw] p-[1.111vw]">
|
||||
<h3 className="title-s flex items-center font-medium gap-[0.556vw]">
|
||||
<span>Документы по сеансу</span> <Badge count={4} />
|
||||
</h3>
|
||||
{/* <div className="flex w-full gap-[0.556vw]">
|
||||
<Button variant="primary" size="large" className="w-full">
|
||||
<span className="w-[1.111vw] h-[1.111vw] text-[#7B60F3]">
|
||||
<DownloadIcon />
|
||||
</span>
|
||||
Скачать архивом
|
||||
</Button>
|
||||
<Button variant="primary" size="large">
|
||||
<span className="w-[1.111vw] h-[1.111vw] text-[#7B60F3]">
|
||||
<ShareIcon />
|
||||
</span>
|
||||
</Button>
|
||||
</div> */}
|
||||
{data && <SessionFiles files={data} session={session} />}
|
||||
</div>
|
||||
{data && (
|
||||
<div className="flex flex-col gap-[1.111vw] bg-white rounded-[1.667vw] p-[1.111vw]">
|
||||
<h3 className="title-s flex items-center font-medium gap-[0.556vw]">
|
||||
<span>Документы по сеансу</span>
|
||||
<Badge count={data?.length} />
|
||||
</h3>
|
||||
<SessionFiles files={data} session={session} />
|
||||
<div className="flex w-full gap-[0.556vw]">
|
||||
<Button variant="primary" size="large" className="w-full">
|
||||
<span className="size-[1.111vw] text-[#7B60F3]">
|
||||
<DownloadIcon />
|
||||
</span>
|
||||
Скачать архивом
|
||||
</Button>
|
||||
<Button variant="primary" size="large">
|
||||
<span className="size-[1.111vw] text-[#7B60F3]">
|
||||
<ShareIcon />
|
||||
</span>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<SessionComments sessionId={session.id} />
|
||||
</div>
|
||||
|
||||
@@ -11,6 +11,10 @@ button {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
button:disabled {
|
||||
color: default;
|
||||
}
|
||||
|
||||
@layer utilities {
|
||||
.text-xs {
|
||||
font-size: 12px;
|
||||
|
||||
@@ -9,7 +9,6 @@ import SessionCard from "../components/SessionCard";
|
||||
import Button from "../components/Button";
|
||||
import ChevronRightIcon from "../components/icons/ChevronRightIcon";
|
||||
import { useNavigate } from "react-router";
|
||||
import NewSelect from "../components/NewSelect";
|
||||
|
||||
function DashboardPage() {
|
||||
const { data: me } = useQuery({
|
||||
@@ -51,7 +50,7 @@ function DashboardPage() {
|
||||
<div className="w-full flex justify-between">
|
||||
<div className="w-[42.5vw] flex flex-col gap-[1.667vw]">
|
||||
<div className="flex items-center gap-[0.833vw]">
|
||||
<h1 className="title-l font-[500] ">Интерактивные столы</h1>
|
||||
<h1 className="title-l font-medium">Интерактивные столы</h1>
|
||||
<Badge count={servers?.length || 0} />
|
||||
</div>
|
||||
<div className="flex gap-[0.833vw] flex-wrap">
|
||||
@@ -61,46 +60,9 @@ function DashboardPage() {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex">
|
||||
<NewSelect
|
||||
data={[
|
||||
{ name: "test" },
|
||||
{ name: "test2" },
|
||||
{ name: "test3" },
|
||||
{ name: "test4" },
|
||||
{ name: "test5" },
|
||||
{ name: "test6" },
|
||||
{ name: "test7" },
|
||||
{ name: "test8" },
|
||||
{ name: "test9" },
|
||||
{ name: "test10" },
|
||||
]}
|
||||
isGrid={false}
|
||||
placeholder="Выберите сервер"
|
||||
resetTitle="Все сервера"
|
||||
/>
|
||||
<NewSelect
|
||||
data={[
|
||||
{ name: "test" },
|
||||
{ name: "test2" },
|
||||
{ name: "test3" },
|
||||
{ name: "test4" },
|
||||
{ name: "test5" },
|
||||
{ name: "test6" },
|
||||
{ name: "test7" },
|
||||
{ name: "test8" },
|
||||
{ name: "test9" },
|
||||
{ name: "test10" },
|
||||
]}
|
||||
isGrid
|
||||
placeholder="Выберите сервер"
|
||||
resetTitle="Все сервера"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="w-full">
|
||||
<div className="flex flex-col gap-[1.667vw]">
|
||||
<h1 className="title-l font-[500] ">Последние сеансы</h1>
|
||||
<h1 className="title-l font-medium">Последние сеансы</h1>
|
||||
<div className="w-full flex flex-col gap-[0.833vw]">
|
||||
<div className="flex flex-col gap-[0.278vw]">
|
||||
{sessions
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import toast from "react-hot-toast";
|
||||
import Input from "../components/Input";
|
||||
import useAuthStore from "../stores/useAuthStore";
|
||||
import api from "../utils/api";
|
||||
import { useState } from "react";
|
||||
@@ -7,6 +6,7 @@ import { HTTPError } from "ky";
|
||||
import { IError } from "../types/Error";
|
||||
import { useNavigate } from "react-router";
|
||||
import Button from "../components/Button";
|
||||
import Input from "../components/Input";
|
||||
|
||||
function LoginPage() {
|
||||
const navigate = useNavigate();
|
||||
@@ -54,7 +54,7 @@ function LoginPage() {
|
||||
<form onSubmit={handleSubmit}>
|
||||
<div>
|
||||
<Input
|
||||
label="Email"
|
||||
placeholder="Email"
|
||||
type="email"
|
||||
required
|
||||
value={email}
|
||||
@@ -63,7 +63,7 @@ function LoginPage() {
|
||||
</div>
|
||||
<div className="mt-6">
|
||||
<Input
|
||||
label="Пароль"
|
||||
placeholder="Пароль"
|
||||
type="password"
|
||||
required
|
||||
value={password}
|
||||
|
||||
+48
-28
@@ -2,8 +2,6 @@ import { useQuery } from "@tanstack/react-query";
|
||||
import api from "../utils/api";
|
||||
import { IUser } from "../types/User";
|
||||
import { Session } from "../types/Session";
|
||||
import Input from "../components/Input";
|
||||
import Select from "../components/Select";
|
||||
import { useState } from "react";
|
||||
import { IApp } from "../types/App";
|
||||
import { useDebounce } from "@uidotdev/usehooks";
|
||||
@@ -11,11 +9,15 @@ import SessionCard from "../components/SessionCard";
|
||||
import { groupByCreatedAt } from "../utils/groupByCreatedAt";
|
||||
import { format, isToday } from "date-fns";
|
||||
import { ru } from "date-fns/locale";
|
||||
import MultySelect from "../components/MultySelect";
|
||||
import Button from "../components/Button";
|
||||
import SearchInput from "../components/SearchInput";
|
||||
|
||||
function SessionsPage() {
|
||||
const [limit, setLimit] = useState(10);
|
||||
const [search, setSearch] = useState<string | null>(null);
|
||||
const [managerId, setManagerId] = useState<string | null>(null);
|
||||
const [appId, setAppId] = useState<string | null>(null);
|
||||
const [managerIds, setManagerIds] = useState<string[]>([]);
|
||||
const [appIds, setAppIds] = useState<string[]>([]);
|
||||
|
||||
const debouncedSearch = useDebounce(search, 500);
|
||||
|
||||
@@ -37,13 +39,15 @@ function SessionsPage() {
|
||||
});
|
||||
|
||||
const { data: grouppedSessions } = useQuery({
|
||||
queryKey: ["sessions", managerId, appId, debouncedSearch],
|
||||
queryKey: ["sessions", managerIds, appIds, debouncedSearch, limit],
|
||||
queryFn: () =>
|
||||
api
|
||||
.get(
|
||||
`sessions?${managerId ? `ownerId=${managerId}` : ""}${
|
||||
appId ? `&appId=${appId}` : ""
|
||||
}${debouncedSearch ? `&clientSearch=${debouncedSearch}` : ""}`
|
||||
`sessions?${
|
||||
managerIds.length ? `ownerIds=${managerIds.join()}` : ""
|
||||
}${appIds.length ? `&appIds=${appIds.join()}` : ""}${
|
||||
debouncedSearch ? `&clientSearch=${debouncedSearch}` : ""
|
||||
}&limit=${limit}`
|
||||
)
|
||||
.json<Session[]>(),
|
||||
enabled: !!me,
|
||||
@@ -51,13 +55,15 @@ function SessionsPage() {
|
||||
});
|
||||
|
||||
const { data: count } = useQuery({
|
||||
queryKey: ["sessions", "count", managerId, appId, debouncedSearch],
|
||||
queryKey: ["sessions", "count", managerIds, appIds, debouncedSearch],
|
||||
queryFn: () =>
|
||||
api
|
||||
.get(
|
||||
`sessions/count?${managerId ? `ownerId=${managerId}` : ""}${
|
||||
appId ? `&appId=${appId}` : ""
|
||||
}${debouncedSearch ? `&clientSearch=${debouncedSearch}` : ""}`
|
||||
`sessions/count?${
|
||||
managerIds.length ? `ownerIds=${managerIds.join()}` : ""
|
||||
}${appIds.length ? `&appIds=${appIds.join()}` : ""}${
|
||||
debouncedSearch ? `&clientSearch=${debouncedSearch}` : ""
|
||||
}`
|
||||
)
|
||||
.json<number>(),
|
||||
enabled: !!me,
|
||||
@@ -69,28 +75,31 @@ function SessionsPage() {
|
||||
<div className="p-[1.389vw] rounded-[2.222vw] shadow-[0_4px_40px_0_rgba(15,16,17,0.05),0_2px_2px_0_rgba(15,16,17,0.05)] w-full">
|
||||
<div className="space-y-[1.111vw]">
|
||||
<div className="flex flex-col gap-[0.556vw]">
|
||||
<Input
|
||||
<SearchInput
|
||||
placeholder="Поиск по имени клиента"
|
||||
value={search || ""}
|
||||
onChange={(e) => setSearch(e.target.value)}
|
||||
onEnter={() => {}}
|
||||
/>
|
||||
<div className="flex gap-[0.556vw]">
|
||||
<Select
|
||||
options={managers?.map((manager) => manager.fullname) || []}
|
||||
onChange={(option) => {
|
||||
setManagerId(
|
||||
managers?.find((manager) => manager.fullname === option)
|
||||
?.id || null
|
||||
);
|
||||
}}
|
||||
<MultySelect
|
||||
data={
|
||||
managers?.map(({ fullname: name, ...manager }) => ({
|
||||
name,
|
||||
...manager,
|
||||
})) || []
|
||||
}
|
||||
isGrid
|
||||
placeholder={"Менеджер"}
|
||||
resetTitle={"Все менеджеры"}
|
||||
onSelect={(values) => setManagerIds(values.map(({ id }) => id))}
|
||||
/>
|
||||
<Select
|
||||
options={apps?.map((app) => app.name) || []}
|
||||
onChange={(option) => {
|
||||
setAppId(
|
||||
apps?.find((app) => app.name === option)?.id || null
|
||||
);
|
||||
}}
|
||||
<MultySelect
|
||||
data={apps || []}
|
||||
isGrid
|
||||
placeholder={"Проект"}
|
||||
resetTitle={"Все проекты"}
|
||||
onSelect={(values) => setAppIds(values.map(({ id }) => id))}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -117,6 +126,17 @@ function SessionsPage() {
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<Button
|
||||
size="large"
|
||||
variant="primary"
|
||||
className="w-full"
|
||||
onClick={() => {
|
||||
setLimit((prev) => prev + 10);
|
||||
}}
|
||||
disabled={!!count && limit >= count}
|
||||
>
|
||||
Показать еще
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
+1
-1
@@ -5,7 +5,7 @@ export interface Server {
|
||||
id: string;
|
||||
hostname: string;
|
||||
name: string;
|
||||
location: string;
|
||||
description: string;
|
||||
companyId: string;
|
||||
sessions?: Session[];
|
||||
apps?: App[];
|
||||
|
||||
Reference in New Issue
Block a user