This commit is contained in:
2025-06-17 15:31:01 +05:00
parent facb76e904
commit 8b8883bb0d
13 changed files with 126 additions and 71 deletions
Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

+1 -1
View File
@@ -1,6 +1,6 @@
function Badge({ count }: { count: number }) {
return (
<div className="size-[1.389vw] rounded-full bg-[#F8F7FE] caption-xs text-[#7B60F3] flex justify-center items-center font-medium font-mono">
<div className="size-[1.389vw] rounded-full bg-[#F8F7FE] caption-xs text-[#7B60F3] flex justify-center items-center font-medium afont-mono">
{count}
</div>
);
+6 -3
View File
@@ -34,8 +34,8 @@ export default function DesktopCard({ server }: IDesktopCardProps) {
>
<Button
variant="secondary"
size="medium"
className="absolute top-[0.347vw] right-[0.347vw] cursor-pointer flex items-center justify-center"
size="small"
className="absolute top-[0.556vw] right-[0.556vw] cursor-pointer flex items-center justify-center"
onClick={() => setModal(<EditTable table={server} />)}
>
<span className="text-[#7D7D7D] w-[0.972vw] h-[0.972vw]">
@@ -84,7 +84,10 @@ export default function DesktopCard({ server }: IDesktopCardProps) {
</Button>
</div>
) : server.status === "offline" ? (
<Button variant="critical" className="hover:bg-[#FEF3F2]">
<Button
variant="critical"
className="hover:bg-[#FEF3F2] !cursor-default"
>
<span className="text-[#FF4517] size-[0.972vw]">
<UnlinkIcon />
</span>
+3 -4
View File
@@ -14,8 +14,7 @@ function SearchInput(
return (
<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]"
"p-[0.556vw] bg-[#F6F6F6] rounded-[0.833vw] w-full flex items-center gap-[1.111vw] hover:bg-[#F0F0F0] px-[1.111vw] py-[1.215vw]"
)}
>
<div className="flex gap-[0.566vw] items-center flex-1">
@@ -51,11 +50,11 @@ function SearchInput(
<CloseIcon />
</div>
</button>
{props.onEnter && (
{/* {pr ops.onEnter && (
<Button size="small" disabled={!props.value} onClick={props.onEnter}>
Искать
</Button>
)}
)} */}
</div>
</div>
);
+1 -1
View File
@@ -1,5 +1,5 @@
import { motion } from "motion/react";
import { Comment } from "../types/Comments";
import { Comment } from "../types/Comment";
import { format } from "date-fns";
function SessionCommentItem({ comment }: { comment: Comment }) {
+30 -9
View File
@@ -1,11 +1,14 @@
import { useRef } from "react";
import { useRef, useState } from "react";
import SendIcon from "./icons/SendIcon";
import Button from "./Button";
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import api from "../utils/api";
import { Comment } from "../types/Comments";
import { Comment } from "../types/Comment";
import SessionCommentItem from "./SessionCommentItem";
import { AnimatePresence } from "motion/react";
import { AnimatePresence, motion } from "motion/react";
import { groupByCreatedAt } from "../utils/groupByCreatedAt";
import { format, isToday } from "date-fns";
import { ru } from "date-fns/locale";
function SessionComments({ sessionId }: { sessionId: string }) {
const textareaRef = useRef<HTMLTextAreaElement>(null);
@@ -49,9 +52,10 @@ function SessionComments({ sessionId }: { sessionId: string }) {
const queryClient = useQueryClient();
const { data: comments } = useQuery({
const { data: grouppedComments } = useQuery({
queryKey: ["sessions", "comments", sessionId],
queryFn: () => api.get(`comments/${sessionId}`).json<Comment[]>(),
select: groupByCreatedAt,
});
const { mutate: createComment } = useMutation({
@@ -76,6 +80,7 @@ function SessionComments({ sessionId }: { sessionId: string }) {
createComment(textarea.value);
textarea.value = "";
setValue("");
handleTextareaInput();
};
@@ -86,13 +91,26 @@ function SessionComments({ sessionId }: { sessionId: string }) {
}
};
const [value, setValue] = useState("");
return (
<div className="outline flex flex-col flex-1 outline-[#D6D6D6]">
<div className="relative h-full flex flex-col-reverse gap-[0.833vw] overflow-y-auto p-[1.111vw] [scrollbar-width:thin]">
<AnimatePresence mode="wait">
{comments && comments.length > 0 ? (
comments.map((comment) => (
<SessionCommentItem key={comment.id} comment={comment} />
{grouppedComments && grouppedComments.length > 0 ? (
grouppedComments.map(([timestamp, comments], index) => (
<motion.div layout className="space-y-[1.111vw]" key={index}>
<p className="text-center text-[#BDBDBD] caption-s">
{isToday(new Date(timestamp))
? "Сегодня"
: format(new Date(timestamp), "d MMMM", { locale: ru })}
</p>
<div className="flex flex-col-reverse gap-[0.833vw]">
{comments.map((comment) => (
<SessionCommentItem key={comment.id} comment={comment} />
))}
</div>
</motion.div>
))
) : (
<div className="flex flex-col gap-[1.111vw] items-center justify-center h-full">
@@ -124,6 +142,9 @@ function SessionComments({ sessionId }: { sessionId: string }) {
name="comment"
className="w-[17.083vw] outline-none text-s resize-none self-center"
placeholder="Расскажите, как все прошло"
onChange={(e) => {
setValue(e.target.value);
}}
style={{
wordWrap: "break-word",
overflowY: "hidden",
@@ -131,8 +152,8 @@ function SessionComments({ sessionId }: { sessionId: string }) {
onInput={handleTextareaInput}
onKeyDown={handleKeyDown}
/>
<Button variant="cta" size="large" type="submit">
<span className="w-[1.111vw] h-[1.111vw] text-white">
<Button variant="cta" size="large" type="submit" disabled={!value}>
<span className="size-[1.111vw] text-white">
<SendIcon />
</span>
</Button>
+3 -2
View File
@@ -1,8 +1,9 @@
function StartSessionIcon() {
return (
<svg viewBox="0 0 7 8" fill="none" xmlns="http://www.w3.org/2000/svg">
<svg viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width={20} height={20} rx={10} fill="#fff" fillOpacity={0.1} />
<path
d="M5.938 3.595a.5.5 0 0 1 0 .81L1.293 7.758A.5.5 0 0 1 .5 7.353V.647a.5.5 0 0 1 .793-.405z"
d="M13.438 9.595a.5.5 0 0 1 0 .81l-4.645 3.353A.5.5 0 0 1 8 13.353V6.647a.5.5 0 0 1 .793-.405z"
fill="currentColor"
/>
</svg>
+18 -18
View File
@@ -226,26 +226,26 @@ export default function CreateSessionModal({ targetServerId }: Props) {
</div>
</div>
)}
<Button
type="submit"
disabled={
!ref.current?.checkValidity() ||
!selectedServer ||
!selectedApp ||
servers?.find((server) => server.id === selectedServer?.id)
?.sessions?.[0]?.status === "ending"
}
variant="cta"
size="large"
className="sticky bottom-0"
>
<div className="rounded-full bg-[#9184F6] in-disabled:bg-transparent px-[0.278vw] py-[0.208vw] size-[1.111vw]">
<div className="w-[0.694vw] h-[0.556vw]">
<div className="flex-1 flex flex-col justify-end">
<Button
type="submit"
disabled={
!ref.current?.checkValidity() ||
!selectedServer ||
!selectedApp ||
servers?.find((server) => server.id === selectedServer?.id)
?.sessions?.[0]?.status === "ending"
}
variant="cta"
size="large"
className="sticky bottom-0"
>
<div className="size-[1.111vw]">
<StartSessionIcon />
</div>
</div>
<p>Запустить сеанс</p>
</Button>
<span>Запустить сеанс</span>
</Button>
</div>
</div>
</form>
);
+1 -1
View File
@@ -18,7 +18,7 @@ function EditTable({ table }: { table: Server }) {
return api.put(`servers/${table.id}`, {
json: {
name: tableName,
location: tableDescription,
description: tableDescription,
},
});
},
+54 -25
View File
@@ -12,6 +12,7 @@ import { ru } from "date-fns/locale";
import MultySelect from "../components/MultySelect";
import Button from "../components/Button";
import SearchInput from "../components/SearchInput";
import CloseIcon from "../components/icons/CloseIcon";
function SessionsPage() {
const [limit, setLimit] = useState(10);
@@ -69,6 +70,12 @@ function SessionsPage() {
enabled: !!me,
});
function reset() {
setSearch("");
setAppIds([]);
setManagerIds([]);
}
return (
<div className="py-[1.667vw] flex flex-col gap-[1.667vw]">
<h1 className="title-l font-medium">Сеансы</h1>
@@ -103,40 +110,62 @@ function SessionsPage() {
/>
</div>
</div>
{!!count && (
<div className="flex justify-between items-center">
<p className="caption-m font-medium opacity-40">
Найдено {count} сеансов
</p>
)}
<button className="flex gap-[0.278vw] items-center" onClick={reset}>
<div className="size-[1.111vw] text-[#7D7D7D]">
<CloseIcon />
</div>
<p className="text-[#7D7D7D] text-[0.972vw] font-medium">
Сбросить все
</p>
</button>
</div>
</div>
</div>
<div className="space-y-[1.667vw]">
{Object.entries(grouppedSessions || {}).map(([timestamp, sessions]) => (
<div key={timestamp} className="space-y-[0.833vw]">
<p className="caption-m font-medium opacity-40">
{isToday(new Date(timestamp))
? "Сегодня"
: format(new Date(timestamp), "d MMMM", { locale: ru })}
</p>
<div className="space-y-[0.278vw]">
{sessions.map((session) => (
<SessionCard key={session.id} session={session} />
))}
{grouppedSessions?.length ? (
grouppedSessions?.map(([timestamp, sessions]) => (
<div key={timestamp} className="space-y-[0.833vw]">
<p className="caption-m font-medium opacity-40">
{isToday(new Date(timestamp))
? "Сегодня"
: format(new Date(timestamp), "d MMMM", { locale: ru })}
</p>
<div className="space-y-[0.278vw]">
{sessions.map((session) => (
<SessionCard key={session.id} session={session} />
))}
</div>
</div>
))
) : (
<div className="m-auto flex flex-col items-center gap-[1.111vw] w-[18.333vw] mt-[6.111vw]">
<img src="/images/sad_ghost.png" alt="" className="w-[13.889vw]" />
<div className="space-y-[0.556vw]">
<p className="text-center font-medium title-m">Ничего не нашли</p>
<p className="text-[#BDBDBD] caption-s font-medium">
Попробуйте изменить параметры поиска
</p>
</div>
</div>
))}
)}
</div>
<Button
size="large"
variant="primary"
className="w-full"
onClick={() => {
setLimit((prev) => prev + 10);
}}
disabled={!!count && limit >= count}
>
Показать еще
</Button>
{!!count && (
<Button
size="large"
variant="primary"
className="w-full"
onClick={() => {
setLimit((prev) => prev + 10);
}}
disabled={!!count && limit >= count}
>
Показать еще
</Button>
)}
</div>
);
}
+1 -1
View File
@@ -1,5 +1,5 @@
import { IApp as App } from "./App";
import { Comment } from "./Comments";
import { Comment } from "./Comment";
import { IOwner as Owner } from "./Owner";
import { Server } from "./Server";
import { Client } from "./Client";
+8 -6
View File
@@ -1,8 +1,10 @@
export function groupByCreatedAt<T extends { createdAt: Date }>(items: T[]) {
return items.reduce((acc, session) => {
const date = session.createdAt.toString().split("T")[0];
acc[date] = acc[date] || [];
acc[date].push(session);
return acc;
}, {} as Record<string, T[]>);
return Object.entries(
items.reduce((acc, session) => {
const date = session.createdAt.toString().split("T")[0];
acc[date] = acc[date] || [];
acc[date].push(session);
return acc;
}, {} as Record<string, T[]>)
);
}