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
+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]">