diff --git a/src/components/Accordion.tsx b/src/components/Accordion.tsx new file mode 100644 index 0000000..dbd2d8c --- /dev/null +++ b/src/components/Accordion.tsx @@ -0,0 +1,59 @@ +import { useState } from "react"; +import ChevronDownIcon from "./icons/ChevronDownIcon"; +import clsx from "clsx"; +import { AnimatePresence, motion } from "motion/react"; +import { useClickAway } from "@uidotdev/usehooks"; + +function Accordion({ text, title }: { title: string; text: string }) { + const [isOpen, setIsOpen] = useState(false); + + const [initialHeight, setInitialHeight] = useState(0); + + const [textHeight, setTextHeight] = useState(0); + + const ref = useClickAway(() => setIsOpen(false)); + + return ( + { + if (el) { + ref.current = el; + setInitialHeight(el?.clientHeight || 0); + } + }} + animate={{ + height: isOpen ? initialHeight + textHeight + 12 : initialHeight, + }} + className="p-[1.111vw] space-y-[0.833vw] bg-[#F6F6F6] rounded-[0.833vw] overflow-hidden cursor-pointer select-none" + onClick={() => setIsOpen(!isOpen)} + > +
+

{title}

+
+ +
+
+ + {isOpen && ( + setTextHeight(el?.clientHeight || 0)} + className="text-s text-[#7D7D7D]" + > + {text} + + )} + +
+ ); +} + +export default Accordion; diff --git a/src/components/DesktopCard.tsx b/src/components/DesktopCard.tsx index 6568fca..50359af 100644 --- a/src/components/DesktopCard.tsx +++ b/src/components/DesktopCard.tsx @@ -12,6 +12,7 @@ import ChevronRightIcon from "./icons/ChevronRightIcon"; import CurrentSessionModal from "./modals/CurrentSessionModal"; import SpinIcon from "./icons/SpinIcon"; import { useIsMutating } from "@tanstack/react-query"; +import { useEffect } from "react"; interface IDesktopCardProps { server: Server; @@ -29,6 +30,10 @@ export default function DesktopCard({ server }: IDesktopCardProps) { setModal(); } + useEffect(() => { + console.log(server.sessions?.[0]?.status); + }, [server.sessions]); + return (
{ + const [currentValue, setCurrentValue] = useState(5); + + return ( +
+

Компонент Шкалы

+ + {/* Интерактивный пример */} +
+

Интерактивный пример

+
+ +
+ + setCurrentValue(Number(e.target.value))} + className="w-40" + /> +
+
+
+ + {/* Различные размеры */} +
+

Различные размеры

+
+ + + +
+
+ + {/* Различные цвета */} +
+

Различные цвета

+
+ + + +
+
+ + {/* Без текста */} +
+

Без отображения значения

+
+ + + +
+
+ + {/* Все значения от 1 до 10 */} +
+

Все значения от 1 до 10

+
+ {Array.from({ length: 10 }, (_, i) => i + 1).map((value) => ( +
+ +

Значение: {value}

+
+ ))} +
+
+
+ ); +}; + +export default ScaleDemo; diff --git a/src/components/ScaleIndicator.tsx b/src/components/ScaleIndicator.tsx new file mode 100644 index 0000000..d696d04 --- /dev/null +++ b/src/components/ScaleIndicator.tsx @@ -0,0 +1,144 @@ +import { motion } from "motion/react"; +import React from "react"; + +interface ScaleIndicatorProps { + value: number; // значение от 1 до 10 + size?: number; // размер в пикселях + strokeWidth?: number; // толщина обводки + backgroundColor?: string; // цвет фона + fillColor?: string; // цвет заполнения + showValue?: boolean; // показывать ли цифру в центре +} + +const ScaleIndicator: React.FC = ({ + value, + size = 64, + strokeWidth = 8, + backgroundColor = "#F0F0F0", + fillColor = "#29AF61", + showValue = true, +}) => { + // Ограничиваем значение от 1 до 10 + const clampedValue = Math.max(1, Math.min(10, value)); + + // Рассчитываем процент заполнения (от 0% до 100%) + const percentage = (clampedValue / 10) * 100; + + // Параметры для дуги + const center = size / 2; + const radius = center - strokeWidth / 2; + + // Функция для конвертации полярных координат в декартовы + const polarToCartesian = ( + centerX: number, + centerY: number, + radius: number, + angleInDegrees: number + ) => { + const angleInRadians = ((angleInDegrees - 90) * Math.PI) / 180.0; + return { + x: centerX + radius * Math.cos(angleInRadians), + y: centerY + radius * Math.sin(angleInRadians), + }; + }; + + // Функция для создания пути дуги + const createArcPath = ( + startAngle: number, + endAngle: number, + radius: number + ) => { + const start = polarToCartesian(center, center, radius, endAngle); + const end = polarToCartesian(center, center, radius, startAngle); + const largeArcFlag = endAngle - startAngle <= 180 ? "0" : "1"; + + return [ + "M", + start.x, + start.y, + "A", + radius, + radius, + 0, + largeArcFlag, + 0, + end.x, + end.y, + ].join(" "); + }; + + // Углы для дуги (270 градусов, как в примере) + const startAngle = 135; // начинаем снизу слева + const totalAngle = 270; // общий угол дуги + const endAngle = startAngle + totalAngle; + + // Рассчитываем угол заполнения + const fillAngle = (percentage / 100) * totalAngle; + const currentEndAngle = startAngle + fillAngle; + + // Пути для фоновой и заполненной дуги + const backgroundPath = createArcPath(startAngle, endAngle, radius); + const fillPath = + fillAngle > 0 ? createArcPath(startAngle, currentEndAngle, radius) : ""; + + // ID для градиента + const gradientId = `gradient-${Math.random().toString(36).substring(2, 9)}`; + + return ( +
+ + + {/* Конический градиент */} + + + + + + + {/* Фоновая дуга */} + + + {/* Заполненная дуга */} + {fillPath && ( + + )} + + {/* Текст со значением в центре */} + {showValue && ( + + {clampedValue} + + )} + +
+ ); +}; + +export default ScaleIndicator; diff --git a/src/components/icons/ChevronDownIcon.tsx b/src/components/icons/ChevronDownIcon.tsx index c51c9a5..7d32f86 100644 --- a/src/components/icons/ChevronDownIcon.tsx +++ b/src/components/icons/ChevronDownIcon.tsx @@ -2,7 +2,7 @@ function ChevronDownIcon() { return ( - // api.put(`sessions/${session.id}`, { - // json: { status: "ending" }, - // }), - // onMutate: () => - // queryClient.invalidateQueries({ - // queryKey: ["sessions"], - // }), - // onSuccess: () => { - // queryClient.invalidateQueries({ - // queryKey: ["last-sessions"], - // }); - // }, - // }); const [now, setNow] = useState(Date.now()); useEffect(() => { @@ -37,6 +22,11 @@ function CurrentSessionModal({ session }: { session: Session }) { return () => clearInterval(interval); }, []); + const { data: client } = useQuery({ + queryKey: ["client", session.clientId], + queryFn: () => api.get(`clients/${session.clientId}`).json(), + }); + if (!session) return null; return ( @@ -82,24 +72,7 @@ function CurrentSessionModal({ session }: { session: Session }) {

Параметры сеанса

-
- -
+ {client && }

Детали

diff --git a/src/components/modals/SessionModal.tsx b/src/components/modals/SessionModal.tsx index d27761e..d9674bc 100644 --- a/src/components/modals/SessionModal.tsx +++ b/src/components/modals/SessionModal.tsx @@ -14,8 +14,12 @@ import SessionFiles from "../SessionFiles"; import DownloadIcon from "../icons/DownloadIcon"; import ShareIcon from "../icons/ShareIcon"; import { Client } from "../../types/Client"; +import useModalStore from "../../stores/useModalStore"; +import SummaryModal from "./SummaryModal"; function SessionModal({ session }: { session: Session }) { + const { setModal } = useModalStore(); + const { data: files } = useQuery({ queryKey: ["file-list", session.id], queryFn: () => @@ -113,16 +117,23 @@ function SessionModal({ session }: { session: Session }) { Бюджет клиента: - 8 500 000 ₽ + + {/* {session.summary.budget}₽ */} +

- Клиент проявил высокий интерес к объекту, особенно к варианту с - улучшенной отделкой. Основной вопрос для принятия решения — - согласование с семьей и выбор этажа. Необходимо подготовить - предварительный договор к следующей встрече. + {/* {session.summary.introduction} */}
-