This commit is contained in:
2025-06-04 19:41:00 +05:00
parent 6da200641a
commit 31f52765f9
12 changed files with 183 additions and 48 deletions
+1 -5
View File
@@ -1,5 +1 @@
<<<<<<< HEAD
VITE_API_URL=http://192.168.1.204:3000/
=======
VITE_API_URL=http://192.168.1.204:3000
>>>>>>> b8d191113582cbbfeaea9d0bea544c445a25c2ac
VITE_API_URL=http://192.168.1.170:3000/
+3
View File
@@ -10,6 +10,7 @@
"@tanstack/react-query": "^5.67.3",
"@uidotdev/usehooks": "^2.4.1",
"clsx": "^2.1.1",
"date-fns": "^4.1.0",
"ky": "^1.7.5",
"motion": "^12.5.0",
"react": "^19.0.0",
@@ -381,6 +382,8 @@
"csstype": ["csstype@3.1.3", "", {}, "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="],
"date-fns": ["date-fns@4.1.0", "", {}, "sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg=="],
"debug": ["debug@4.4.0", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA=="],
"dedent": ["dedent@1.5.3", "", { "peerDependencies": { "babel-plugin-macros": "^3.1.0" }, "optionalPeers": ["babel-plugin-macros"] }, "sha512-NHQtfOOW68WD8lgypbLA5oT+Bt0xXJhiYvoR6SmmNXZfpzOGXwdKWmcwG8N7PwVVWV3eF/68nmD9BaJSsTBhyQ=="],
+1
View File
@@ -16,6 +16,7 @@
"@tanstack/react-query": "^5.67.3",
"@uidotdev/usehooks": "^2.4.1",
"clsx": "^2.1.1",
"date-fns": "^4.1.0",
"ky": "^1.7.5",
"motion": "^12.5.0",
"react": "^19.0.0",
+68
View File
@@ -0,0 +1,68 @@
import FlashIcon from "./icons/FlashIcon";
import { ISession } from "../types/ISession";
import NewButton from "./NewButton";
import api from "../utils/api";
import { useMutation, useQueryClient } from "@tanstack/react-query";
import ChevronRightIcon from "./icons/ChevronRightIcon";
import { motion } from "motion/react";
function CurrentSessionCard({
session,
index,
}: {
session: ISession;
index: number;
}) {
const queryClient = useQueryClient();
const { mutate: endSession } = useMutation({
mutationKey: ["sessions", session.id],
mutationFn: () =>
api.put(`sessions/${session.id}`, { json: { status: "ending" } }),
onMutate: () => {
queryClient.invalidateQueries({ queryKey: ["sessions"] });
queryClient.invalidateQueries({ queryKey: ["last-started"] });
queryClient.invalidateQueries({ queryKey: ["servers"] });
},
});
return (
<motion.div
key={session.id}
layout
initial={{ x: "100%" }}
animate={{ x: "0%" }}
exit={{ x: "100%" }}
transition={{ bounce: 0, delay: index * 0.1 }}
className="p-[1.389vw] rounded-[1.667vw] bg-white w-[18.889vw] flex flex-col gap-[0.833vw] shadow-[0px_4px_40px_0_rgba(0,0,0,0.05),0px_2px_2px_0_rgba(0,0,0,0.05)]"
>
<div className="flex justify-between gap-[0.833vw] items-center">
<span className="size-[1.111vw] text-[#7B60F3]">
<FlashIcon />
</span>
<div className="flex flex-col items-center gap-y-[0.278vw]">
<p className="button-m font-medium text-[#7B60F3] text-center">
Текущий сеанс
</p>
<div className="flex items-center gap-[0.278vw]">
<p className="caption-s font-medium text-[#7D7D7D]">
{session.server.name}
</p>
<div className="size-[0.139vw] bg-[#7D7D7D] rounded-full" />
<p className="caption-s font-medium text-[#7D7D7D]">
{session.owner.fullname}
</p>
</div>
</div>
<span className="size-[1.111vw] text-[#7D7D7D]">
<ChevronRightIcon />
</span>
</div>
<NewButton variant="critical" onClick={() => endSession()}>
Завершить сеанс
</NewButton>
</motion.div>
);
}
export default CurrentSessionCard;
+28 -2
View File
@@ -1,15 +1,41 @@
import { Outlet } from "react-router";
import Navbar from "./Navbar";
import { useQuery } from "@tanstack/react-query";
import api from "../utils/api";
import CurrentSessionCard from "./CurrentSessionCard";
import { ISession } from "../types/ISession";
import { AnimatePresence } from "motion/react";
function Layout() {
const { data: currentStartedSessions } = useQuery({
queryKey: ["sessions", "last-started"],
queryFn: () =>
api
.get("sessions", {
searchParams: { limit: 3, status: "started" },
})
.json<ISession[]>(),
refetchInterval: 1000,
});
return (
<div className="flex">
<div className="flex gap-[1.667vw] overflow-hidden">
<div className="flex-1"></div>
<div className="w-[42.5vw] flex flex-col gap-[1.667vw]">
<Navbar />
<Outlet />
</div>
<div className="flex-1"></div>
<div className="flex-1 flex flex-col items-center gap-y-[0.833vw] py-[1.667vw]">
<AnimatePresence>
{currentStartedSessions?.map((session, index, { length }) => (
<CurrentSessionCard
key={session.id}
session={session}
index={length - index}
/>
))}
</AnimatePresence>
</div>
</div>
);
}
+6 -7
View File
@@ -1,14 +1,13 @@
import { ISession } from "../types/ISession";
function SessionCard({ session }: { session: ISession }) {
console.log(session);
return (
<div className='w-full h-[4.444vw] border-1 border-l-0 border-r-0 border-t-0 border-b-[#F6F6F6] flex py-[0.278vw] items-center gap-[0.556vw] cursor-pointer group'>
<div className='rounded-xl w-full h-full flex items-center gap-[0.556vw] group-hover:bg-[#F6F6F6] transition-colors duration-200'>
<div className='size-[2.5vw] bg-[#F6F6F6] rounded-full'></div>
<div className='flex flex-col w-full gap-[0.278vw]'>
<p className='button-m font-medium'>{session.owner.fullname}</p>
<p className='caption-s font-medium text-[#7D7D7D]'>
<div className="w-full h-[4.444vw] border-1 border-l-0 border-r-0 border-t-0 border-b-[#F6F6F6] flex py-[0.278vw] items-center gap-[0.556vw] cursor-pointer group">
<div className="rounded-xl w-full h-full flex items-center gap-[0.556vw] group-hover:bg-[#F6F6F6] transition-colors duration-200">
<div className="size-[2.5vw] bg-[#F6F6F6] rounded-full"></div>
<div className="flex flex-col w-full gap-[0.278vw]">
<p className="button-m font-medium">{session.owner.fullname}</p>
<p className="caption-s font-medium text-[#7D7D7D]">
Клиент: {session.client.name}&nbsp;&nbsp;
{session.app.name}
</p>
+5 -7
View File
@@ -1,15 +1,13 @@
function FlashIcon() {
return (
<svg
width={20}
height={20}
viewBox='0 0 20 20'
fill='currentColor'
xmlns='http://www.w3.org/2000/svg'
viewBox="0 0 20 20"
fill="currentColor"
xmlns="http://www.w3.org/2000/svg"
>
<path
d='M8.71094 3H13.62L10.62 7.9H14.7109L8.16548 17L9.52912 10.35H5.71094L8.71094 3Z'
fill='currentColor'
d="M8.71094 3H13.62L10.62 7.9H14.7109L8.16548 17L9.52912 10.35H5.71094L8.71094 3Z"
fill="currentColor"
/>
</svg>
);
+4 -2
View File
@@ -28,6 +28,7 @@ export default function CreateSessionModal({
targetServer
);
const [selectedApp, setSelectedApp] = useState<IApp | null>(null);
const queryClient = useQueryClient();
const { mutate: createClient } = useMutation({
@@ -56,8 +57,9 @@ export default function CreateSessionModal({
})
.json<ISession>();
},
onSuccess: () => {
onMutate: () => {
queryClient.invalidateQueries({ queryKey: ["sessions"] });
queryClient.invalidateQueries({ queryKey: ["last-started"] });
queryClient.invalidateQueries({ queryKey: ["servers"] });
setModal(null);
},
@@ -121,7 +123,7 @@ export default function CreateSessionModal({
</div>
<div className="flex flex-col gap-y-[0.833vw]">
<p className="title-s font-medium">Выберите параметры сеанса</p>
{selectedServer?.apps?.length && selectedServer?.apps?.length > 0 && (
{selectedServer?.apps && selectedServer?.apps?.length > 0 && (
<ProjectSelector
projects={selectedServer?.apps || []}
selectedProject={selectedApp ?? selectedServer?.apps?.[0] ?? null}
+51 -6
View File
@@ -1,3 +1,4 @@
import { intervalToDuration } from "date-fns";
import { IServer } from "../../types/IServer";
import FlashIcon from "../icons/FlashIcon";
import NewButton from "../NewButton";
@@ -6,6 +7,7 @@ import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import api from "../../utils/api";
import useModalStore from "../../stores/useModalStore";
import { ISession } from "../../types/ISession";
import { useEffect, useState } from "react";
function CurrentSessionModal({ server }: { server: IServer }) {
const queryClient = useQueryClient();
@@ -20,13 +22,36 @@ function CurrentSessionModal({ server }: { server: IServer }) {
onMutate: () => queryClient.invalidateQueries({ queryKey: ["sessions"] }),
});
const { data: currentServer } = useQuery({
const { data: currentSession } = useQuery({
queryKey: ["sessions", server.sessions?.[0]?.id],
queryFn: () =>
api.get(`sessions/${server.sessions?.[0]?.id}`).json<ISession>(),
});
console.log(currentServer);
console.log(currentSession);
const [now, setNow] = useState(Date.now());
useEffect(() => {
const interval = setInterval(() => {
setNow(Date.now());
}, 1000);
return () => clearInterval(interval);
}, []);
useEffect(() => {
if (!currentSession) return;
const duration = intervalToDuration({
start: currentSession.createdAt,
end: now,
});
const hours = (duration.hours || 0).toString().padStart(2, "0");
const minutes = (duration.minutes || 0).toString().padStart(2, "0");
const seconds = (duration.seconds || 0).toString().padStart(2, "0");
console.log(`${hours}:${minutes}:${seconds}`);
}, [currentSession, now]);
if (!currentSession) return null;
return (
<div className="w-[25vw] bg-[#FFFFFF] rounded-4xl px-[1.389vw] pb-[1.389vw]">
@@ -41,7 +66,23 @@ function CurrentSessionModal({ server }: { server: IServer }) {
<p className="title-s font-medium">{server.name}</p>
<p className="flex justify-center items-center gap-[0.139vw] caption-s font-medium text-[#7B60F3]">
<FlashIcon />
Сеанс идёт 24:05
Сеанс идёт{" "}
{(() => {
const duration = intervalToDuration({
start: currentSession.createdAt,
end: now,
});
const hours = (duration.hours || 0)
.toString()
.padStart(2, "0");
const minutes = (duration.minutes || 0)
.toString()
.padStart(2, "0");
const seconds = (duration.seconds || 0)
.toString()
.padStart(2, "0");
return `${hours}:${minutes}:${seconds}`;
})()}
</p>
</div>
<div>
@@ -57,10 +98,12 @@ function CurrentSessionModal({ server }: { server: IServer }) {
<NewButton variant="secondary" className="w-full">
<div className="flex flex-col gap-[0.278vw] w-full text-left h-[2.222vw]">
<p className="caption-s font-medium text-[#BDBDBD]">Клиент</p>
<p className="text-s font-medium">{server.client?.name}</p>
<p className="text-s font-medium">
{currentSession.client.name}
</p>
</div>
<div className="flex gap-[0.556vw] items-center">
{!server.client?.email && (
{!currentSession.client.email && (
<p className="caption-s font-medium text-[#7B60F3] whitespace-nowrap">
Добавьте email
</p>
@@ -77,7 +120,9 @@ function CurrentSessionModal({ server }: { server: IServer }) {
<div>
<div className="flex gap-[0.556vw]">
<p className="caption-s font-medium text-[#BDBDBD]">Менеджер:</p>
<p className="caption-s font-medium">{server.owner?.fullname}</p>
<p className="caption-s font-medium">
{currentSession.owner.fullname}
</p>
</div>
<div className="flex gap-[0.556vw]">
<p className="caption-s font-medium text-[#BDBDBD]">
+15 -15
View File
@@ -43,25 +43,25 @@ function DashboardPage() {
// }
return (
<div className='flex flex-col gap-[5vw] min-h-dvh'>
<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>
<div className="flex flex-col gap-[5vw] min-h-dvh">
<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>
<Badge count={servers?.length || 0} />
</div>
<div className='flex gap-[0.833vw] flex-wrap'>
<div className="flex gap-[0.833vw] flex-wrap">
{servers?.map((server) => (
<DesktopCard key={server.id} server={server} />
))}
</div>
</div>
</div>
<div className='w-full'>
<div className='flex flex-col gap-[1.667vw]'>
<h1 className='title-l font-[500] '>Последние сеансы</h1>
<div className='w-full flex flex-col gap-[0.833vw]'>
<div className='flex flex-col gap-[0.278vw]'>
<div className="w-full">
<div className="flex flex-col gap-[1.667vw]">
<h1 className="title-l font-[500] ">Последние сеансы</h1>
<div className="w-full flex flex-col gap-[0.833vw]">
<div className="flex flex-col gap-[0.278vw]">
{sessions
?.filter((session) => session.status === "ended")
.map((session) => (
@@ -69,12 +69,12 @@ function DashboardPage() {
))}
</div>
<NewButton
variant='primary'
size='large'
className='flex gap-[0.556vw] items-center justify-center'
variant="primary"
size="large"
className="flex gap-[0.556vw] items-center justify-center"
>
<p>Смотреть всё</p>
<span className='w-[1.111vw] h-[1.111vw] flex items-center justify-center'>
<span className="w-[1.111vw] h-[1.111vw] flex items-center justify-center">
<ChevronRightIcon />
</span>
</NewButton>
-4
View File
@@ -1,6 +1,4 @@
import { IApp } from "./IApp";
import { IClient } from "./IClient";
import { IOwner } from "./IOwner";
import { ISession } from "./ISession";
export interface IServer {
@@ -12,6 +10,4 @@ export interface IServer {
sessions?: ISession[];
apps?: IApp[];
status: "online" | "offline";
client?: IClient;
owner?: IOwner;
}
+1
View File
@@ -14,4 +14,5 @@ export interface ISession {
client: IUser;
app: IApp;
owner: IOwner;
createdAt: Date;
}