diff --git a/CHANGELOG.md b/CHANGELOG.md index b986a9bc..db32b108 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,4 @@ +1.0.6 _ 2025-09-19 _ логика включения таймера 1.0.5 _ 2025-09-18 _ особые условия для вывода картинок 1.0.4 _ 2025-09-14 _ особые условия для вывода картинок 1.0.3 _ 2025-09-12 _ среднее время не учитывает нули diff --git a/src/pages/startPage/StartPageSettings/BackBlockedWithTooltip.tsx b/src/pages/startPage/StartPageSettings/BackBlockedWithTooltip.tsx new file mode 100644 index 00000000..b91cb484 --- /dev/null +++ b/src/pages/startPage/StartPageSettings/BackBlockedWithTooltip.tsx @@ -0,0 +1,81 @@ +import { Box, ClickAwayListener, Tooltip, Typography, useTheme } from "@mui/material"; +import CustomizedSwitch from "@/ui_kit/CustomSwitch"; +import { useState } from "react"; +import { useCurrentQuiz } from "@/stores/quizes/hooks"; +import { updateQuiz } from "@/stores/quizes/actions"; + +export default function BackBlockedWithTooltip() { + const theme = useTheme(); + const quiz = useCurrentQuiz(); + const [open, setOpen] = useState(false); + + if (!quiz) return null; + + const enabled = Boolean((quiz as any)?.config?.questionTimerEnabled); + const checked = Boolean((quiz as any)?.config?.backBlocked); + + const handleTooltipClose = () => setOpen(false); + const handleTooltipOpen = () => setOpen(true); + + const tooltipText = "В режиме опроса с таймером, кнопка назад не работает"; + + return ( + + + {enabled && ( + + )} + + + { + updateQuiz(quiz.id, (q) => { + (q as any).config.backBlocked = e.target.checked; + }); + }} + disabled={enabled} + /> + + Запретить шаг назад + + + + + + ); +} + + diff --git a/src/pages/startPage/StartPageSettings/QuestionTimerSettings.tsx b/src/pages/startPage/StartPageSettings/QuestionTimerSettings.tsx new file mode 100644 index 00000000..dd47347a --- /dev/null +++ b/src/pages/startPage/StartPageSettings/QuestionTimerSettings.tsx @@ -0,0 +1,225 @@ +import { Box, Typography, useMediaQuery, useTheme } from "@mui/material"; +import CustomTextField from "@/ui_kit/CustomTextField"; +import moment from "moment"; +import { useEffect, useMemo, useRef, useState } from "react"; +import { updateQuiz } from "@root/quizes/actions"; +import { useCurrentQuiz } from "@root/quizes/hooks"; +import CustomizedSwitch from "@ui_kit/CustomSwitch"; + +export default function QuestionTimerSettings() { + const theme = useTheme(); + const isMobile = useMediaQuery(theme.breakpoints.down(650)); + const quiz = useCurrentQuiz(); + const minutesRef = useRef(null); + const secondsRef = useRef(null); + const enabled = Boolean((quiz as any)?.config?.questionTimerEnabled); + + const initialSeconds = useMemo(() => { + // Читаем из корректного поля, с fallback на возможное старое имя + const raw = quiz?.config.time_of_passing ?? 0; + const sec = Number(raw) || 0; + return Math.max(0, sec); + }, [quiz?.config.time_of_passing]); + + const initial = useMemo(() => { + const d = moment.duration(initialSeconds, "seconds"); + const m = Math.min(60, Math.max(0, d.minutes() + d.hours() * 60)); + const s = Math.min(60, Math.max(0, d.seconds())); + return { + minutes: String(m), + seconds: String(s), + }; + }, [initialSeconds]); + + const [minutes, setMinutes] = useState(initial.minutes); + const [seconds, setSeconds] = useState(initial.seconds); + + // Не синхронизируем обратно каждое изменение, чтобы не ломать ввод + + const toDigits = (value: string): string => (value || "").replace(/\D+/g, ""); + + const clampTwoDigits = (value: string): string => { + const digits = toDigits(value).slice(0, 2); + if (digits.length === 0) return ""; + // если пользователь выделил всё и печатает заново, берём именно введённые цифры + const num = Number(digits); + if (isNaN(num)) return ""; + if (num > 60) return "60"; + return digits; // не форматируем и не обнуляем ведущие нули + }; + + const persist = (mStr: string, sStr: string) => { + const m = Number(toDigits(mStr) || 0); + const s = Number(toDigits(sStr) || 0); + const total = Math.min(3660, Math.max(0, (isNaN(m) ? 0 : m) * 60 + (isNaN(s) ? 0 : s))); + updateQuiz(quiz!.id, (q) => { + // Пишем только в корректное поле модели + (q as any).config.time_of_passing = total; + }); + }; + + const allowControlKey = (e: React.KeyboardEvent) => { + const allowed = [ + "Backspace", + "Delete", + "ArrowLeft", + "ArrowRight", + "Tab", + "Home", + "End", + "Enter", + ]; + return allowed.includes(e.key); + }; + + const handleDigitKeyDown = (e: React.KeyboardEvent) => { + if (allowControlKey(e)) return; + if (!/^[0-9]$/.test(e.key)) { + e.preventDefault(); + } + }; + + const handleMinutesPaste = (e: React.ClipboardEvent) => { + e.preventDefault(); + const text = e.clipboardData.getData("text"); + const next = clampTwoDigits(text); + setMinutes(next); + if (toDigits(next).length >= 2 && secondsRef.current) { + secondsRef.current.focus(); + secondsRef.current.select(); + } + persist(next, seconds); + }; + + const handleSecondsPaste = (e: React.ClipboardEvent) => { + e.preventDefault(); + const text = e.clipboardData.getData("text"); + const next = clampTwoDigits(text); + setSeconds(next); + persist(minutes, next); + }; + + return ( + + + { + updateQuiz(quiz!.id, (q) => { + (q as any).config.questionTimerEnabled = e.target.checked; + (q as any).config.backBlocked = true; + }); + }} + /> + + + Включить таймер вопросов + + + + + + { + const next = clampTwoDigits(e.target.value); + setMinutes(next); + if (toDigits(next).length === 2 && secondsRef.current) { + secondsRef.current.focus(); + secondsRef.current.select(); + } + persist(next, seconds); + }} + onFocus={() => minutesRef.current?.select()} + onClick={() => minutesRef.current?.select()} + onKeyDown={handleDigitKeyDown as any} + onPaste={handleMinutesPaste as any} + InputProps={{ inputProps: { pattern: "\\d*", inputMode: "numeric" } }} + sx={{ + width: "51px", + height: "48px", + }} + inputRef={minutesRef} + /> + + мин. + + + + + { + const next = clampTwoDigits(e.target.value); + setSeconds(next); + persist(minutes, next); + }} + onFocus={() => secondsRef.current?.select()} + onClick={() => secondsRef.current?.select()} + onKeyDown={handleDigitKeyDown as any} + onPaste={handleSecondsPaste as any} + InputProps={{ inputProps: { pattern: "\\d*", inputMode: "numeric" } }} + sx={{ + width: "51px", + height: "48px", + }} + inputRef={secondsRef} + /> + + сек. + + + + + ); +} + + diff --git a/src/pages/startPage/StartPageSettings.tsx b/src/pages/startPage/StartPageSettings/StartPageSettings.tsx similarity index 89% rename from src/pages/startPage/StartPageSettings.tsx rename to src/pages/startPage/StartPageSettings/StartPageSettings.tsx index effea858..6ea822f6 100755 --- a/src/pages/startPage/StartPageSettings.tsx +++ b/src/pages/startPage/StartPageSettings/StartPageSettings.tsx @@ -31,20 +31,22 @@ import { incrementCurrentStep, updateQuiz, uploadQuizImage } from "@root/quizes/ import { useCurrentQuiz } from "@root/quizes/hooks"; import CustomCheckbox from "@ui_kit/CustomCheckbox"; import CustomTextField from "@ui_kit/CustomTextField"; +import QuestionTimerSettings from "./QuestionTimerSettings"; import SelectableButton from "@ui_kit/SelectableButton"; import { StartPagePreview } from "@ui_kit/StartPagePreview"; import { resizeFavIcon } from "@ui_kit/reactImageFileResizer"; import { useState } from "react"; import { createPortal } from "react-dom"; -import FaviconDropZone from "./FaviconDropZone"; -import ModalSizeImage from "./ModalSizeImage"; -import SelectableIconButton from "./SelectableIconButton"; -import { DropZone } from "./dropZone"; -import Extra from "./extra"; +import FaviconDropZone from "../FaviconDropZone"; +import ModalSizeImage from "../ModalSizeImage"; +import SelectableIconButton from "../SelectableIconButton"; +import { DropZone } from "../dropZone"; +import Extra from "../extra"; import TooltipClickInfo from "@ui_kit/Toolbars/TooltipClickInfo"; -import { VideoElement } from "./VideoElement"; -import UploadVideoModal from "../Questions/UploadVideoModal"; +import { VideoElement } from "../VideoElement"; +import UploadVideoModal from "../../Questions/UploadVideoModal"; import { SwitchAI } from "@/ui_kit/crutchFunctionAI"; +import BackBlockedWithTooltip from "./BackBlockedWithTooltip"; const designTypes = [ ["standard", (color: string) => , "Standard"], @@ -875,118 +877,8 @@ export default function StartPageSettings() { Включить защиту от копирования - - { - updateQuiz(quiz.id, (quiz) => { - quiz.config.backBlocked = e.target.checked; - }); - }} - /> - - Запретить шаг назад - - - - - { - updateQuiz(quiz.id, (quiz) => { - quiz.config.backBlocked = e.target.checked; - }); - }} - /> - - - Включить таймер вопросов - - - - - - updateQuiz(quiz.id, (quiz) => { - quiz.config.info.law = e.target.value; - }) - } - sx={{ - width: "51px", - height: "48px" - }} - /> - - мин. - - - - - - - updateQuiz(quiz.id, (quiz) => { - quiz.config.info.law = e.target.value; - }) - } - sx={{ - width: "51px", - height: "48px" - }} - /> - - сек. - - - - - + + {!isSmallMonitor && } )} diff --git a/src/ui_kit/CustomTextField.tsx b/src/ui_kit/CustomTextField.tsx index 900eb433..75621450 100755 --- a/src/ui_kit/CustomTextField.tsx +++ b/src/ui_kit/CustomTextField.tsx @@ -1,4 +1,4 @@ -import type { ChangeEvent, FocusEvent, KeyboardEvent } from "react"; +import type { ChangeEvent, FocusEvent, KeyboardEvent, MouseEvent, ClipboardEvent, Ref } from "react"; import React, { useEffect, useState } from "react"; import type { InputProps, SxProps, Theme } from "@mui/material"; import { @@ -20,6 +20,9 @@ interface CustomTextFieldProps { onChange?: (event: ChangeEvent) => void; onKeyDown?: (event: KeyboardEvent) => void; onBlur?: (event: FocusEvent) => void; + onFocus?: (event: FocusEvent) => void; + onClick?: (event: MouseEvent) => void; + onPaste?: (event: ClipboardEvent) => void; text?: string; maxLength?: number; sx?: SxProps; @@ -29,6 +32,7 @@ interface CustomTextFieldProps { rows?: number; className?: string; disabled?: boolean; + inputRef?: Ref; } export default function CustomTextField({ @@ -38,6 +42,9 @@ export default function CustomTextField({ onChange, onKeyDown, onBlur, + onFocus, + onClick, + onPaste, text, sx, error, @@ -49,6 +56,7 @@ export default function CustomTextField({ sxForm, className, disabled, + inputRef, }: CustomTextFieldProps) { const theme = useTheme(); @@ -77,8 +85,9 @@ export default function CustomTextField({ } }; - const handleInputFocus = () => { + const handleInputFocus = (event: React.FocusEvent) => { setIsInputActive(true); + if (onFocus) onFocus(event); }; const handleInputBlur = (event: React.FocusEvent) => { @@ -117,10 +126,14 @@ export default function CustomTextField({ onFocus={handleInputFocus} onBlur={handleInputBlur} onKeyDown={onKeyDown} + onClick={onClick} + onPaste={onPaste} multiline={rows > 0} rows={rows} disabled={disabled} disableUnderline + inputRef={inputRef} + {...InputProps} sx={{ maxLength: maxLength, borderRadius: "10px", diff --git a/src/ui_kit/switchStepPages.tsx b/src/ui_kit/switchStepPages.tsx index 26d5366c..a6c22ce1 100755 --- a/src/ui_kit/switchStepPages.tsx +++ b/src/ui_kit/switchStepPages.tsx @@ -16,7 +16,7 @@ const FormQuestionsPage = lazy( const QuestionsPage = lazy(() => import("../pages/Questions/QuestionsPage")); const ResultPage = lazy(() => import("../pages/ResultPage/ResultPage")); const StartPageSettings = lazy( - () => import("../pages/startPage/StartPageSettings"), + () => import("../pages/startPage/StartPageSettings/StartPageSettings"), ); const StepOne = lazy(() => import("../pages/startPage/stepOne")); const Steptwo = lazy(() => import("../pages/startPage/steptwo"));