при выборе таймера выключается шаг назад

This commit is contained in:
Nastya 2025-09-20 01:38:50 +03:00
parent 3e122600e5
commit f0c8cfd32b
5 changed files with 333 additions and 122 deletions

@ -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 (
<ClickAwayListener onClickAway={handleTooltipClose}>
<Box sx={{ position: "relative", display: "flex", gap: "20px", alignItems: "center", mt: "20px" }}>
{enabled && (
<Box
sx={{
position: "absolute",
inset: 0,
zIndex: 1,
cursor: "help",
borderRadius: "8px",
}}
onMouseEnter={handleTooltipOpen}
onMouseLeave={handleTooltipClose}
onClick={handleTooltipOpen}
/>
)}
<Tooltip
PopperProps={{
disablePortal: true,
sx: {
"& .MuiTooltip-tooltip": {
fontSize: "14px",
padding: "12px",
maxWidth: "300px",
whiteSpace: "pre-line",
},
},
}}
placement="top"
onClose={handleTooltipClose}
open={open && enabled}
title={tooltipText}
>
<Box sx={{ display: "flex", gap: "20px", alignItems: "center" }}>
<CustomizedSwitch
checked={checked}
onChange={(e) => {
updateQuiz(quiz.id, (q) => {
(q as any).config.backBlocked = e.target.checked;
});
}}
disabled={enabled}
/>
<Typography
sx={{
fontWeight: 500,
color: theme.palette.grey3.main,
}}
>
Запретить шаг назад
</Typography>
</Box>
</Tooltip>
</Box>
</ClickAwayListener>
);
}

@ -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<HTMLInputElement | null>(null);
const secondsRef = useRef<HTMLInputElement | null>(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<string>(initial.minutes);
const [seconds, setSeconds] = useState<string>(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<HTMLInputElement>) => {
const allowed = [
"Backspace",
"Delete",
"ArrowLeft",
"ArrowRight",
"Tab",
"Home",
"End",
"Enter",
];
return allowed.includes(e.key);
};
const handleDigitKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
if (allowControlKey(e)) return;
if (!/^[0-9]$/.test(e.key)) {
e.preventDefault();
}
};
const handleMinutesPaste = (e: React.ClipboardEvent<HTMLInputElement>) => {
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<HTMLInputElement>) => {
e.preventDefault();
const text = e.clipboardData.getData("text");
const next = clampTwoDigits(text);
setSeconds(next);
persist(minutes, next);
};
return (
<Box
sx={{
display: "flex",
alignItems: "center",
mt: "10px",
flexWrap: "wrap",
gap: isMobile ? "20px" : "34px",
}}
>
<Box sx={{ display: "flex", alignItems: "center", gap: "20px" }}>
<CustomizedSwitch
checked={enabled}
onChange={(e) => {
updateQuiz(quiz!.id, (q) => {
(q as any).config.questionTimerEnabled = e.target.checked;
(q as any).config.backBlocked = true;
});
}}
/>
<Typography
sx={{
fontWeight: 500,
color: theme.palette.grey3.main,
}}
>
Включить таймер вопросов
</Typography>
</Box>
<Box
sx={{
display: "flex",
gap: "15px",
}}
>
<Box
sx={{
display: "inline-flex",
alignItems: "center",
}}
>
<CustomTextField
id="question-timer-minutes"
placeholder="00"
type="tel"
value={minutes}
disabled={!enabled}
onChange={(e) => {
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}
/>
<Typography
sx={{
fontWeight: 500,
color: theme.palette.grey3.main,
ml: "5px",
}}
>
мин.
</Typography>
</Box>
<Box
sx={{
display: "inline-flex",
alignItems: "center",
}}
>
<CustomTextField
id="question-timer-seconds"
placeholder="00"
type="tel"
value={seconds}
disabled={!enabled}
onChange={(e) => {
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}
/>
<Typography
sx={{
fontWeight: 500,
color: theme.palette.grey3.main,
ml: "5px",
}}
>
сек.
</Typography>
</Box>
</Box>
</Box>
);
}

@ -31,20 +31,22 @@ import { incrementCurrentStep, updateQuiz, uploadQuizImage } from "@root/quizes/
import { useCurrentQuiz } from "@root/quizes/hooks"; import { useCurrentQuiz } from "@root/quizes/hooks";
import CustomCheckbox from "@ui_kit/CustomCheckbox"; import CustomCheckbox from "@ui_kit/CustomCheckbox";
import CustomTextField from "@ui_kit/CustomTextField"; import CustomTextField from "@ui_kit/CustomTextField";
import QuestionTimerSettings from "./QuestionTimerSettings";
import SelectableButton from "@ui_kit/SelectableButton"; import SelectableButton from "@ui_kit/SelectableButton";
import { StartPagePreview } from "@ui_kit/StartPagePreview"; import { StartPagePreview } from "@ui_kit/StartPagePreview";
import { resizeFavIcon } from "@ui_kit/reactImageFileResizer"; import { resizeFavIcon } from "@ui_kit/reactImageFileResizer";
import { useState } from "react"; import { useState } from "react";
import { createPortal } from "react-dom"; import { createPortal } from "react-dom";
import FaviconDropZone from "./FaviconDropZone"; import FaviconDropZone from "../FaviconDropZone";
import ModalSizeImage from "./ModalSizeImage"; import ModalSizeImage from "../ModalSizeImage";
import SelectableIconButton from "./SelectableIconButton"; import SelectableIconButton from "../SelectableIconButton";
import { DropZone } from "./dropZone"; import { DropZone } from "../dropZone";
import Extra from "./extra"; import Extra from "../extra";
import TooltipClickInfo from "@ui_kit/Toolbars/TooltipClickInfo"; import TooltipClickInfo from "@ui_kit/Toolbars/TooltipClickInfo";
import { VideoElement } from "./VideoElement"; import { VideoElement } from "../VideoElement";
import UploadVideoModal from "../Questions/UploadVideoModal"; import UploadVideoModal from "../../Questions/UploadVideoModal";
import { SwitchAI } from "@/ui_kit/crutchFunctionAI"; import { SwitchAI } from "@/ui_kit/crutchFunctionAI";
import BackBlockedWithTooltip from "./BackBlockedWithTooltip";
const designTypes = [ const designTypes = [
["standard", (color: string) => <LayoutStandartIcon color={color} />, "Standard"], ["standard", (color: string) => <LayoutStandartIcon color={color} />, "Standard"],
@ -875,118 +877,8 @@ export default function StartPageSettings() {
Включить защиту от копирования Включить защиту от копирования
</Typography> </Typography>
</Box> </Box>
<Box sx={{ display: "flex", gap: "20px", alignItems: "center", mt: "20px" }}> <BackBlockedWithTooltip />
<CustomizedSwitch <QuestionTimerSettings />
checked={quiz.config?.backBlocked}
onChange={(e) => {
updateQuiz(quiz.id, (quiz) => {
quiz.config.backBlocked = e.target.checked;
});
}}
/>
<Typography
sx={{
fontWeight: 500,
color: theme.palette.grey3.main,
}}
>
Запретить шаг назад
</Typography>
</Box>
<Box sx={{ display: "flex", alignItems: "center", mt: "20px", flexWrap: "wrap",
gap: isMobile ? "20px" : "34px" }}>
<Box sx ={{
display: "flex",
alignItems: "center",
gap: "20px"
}}>
<CustomizedSwitch
checked={quiz.config?.backBlocked}
onChange={(e) => {
updateQuiz(quiz.id, (quiz) => {
quiz.config.backBlocked = e.target.checked;
});
}}
/>
<Typography
sx={{
fontWeight: 500,
color: theme.palette.grey3.main,
}}
>
Включить таймер вопросов
</Typography>
</Box>
<Box
sx={{
display: "flex",
gap: "15px"
}}
>
<Box
sx={{
display: "inline-flex",
alignItems: "center"
}}
>
<CustomTextField
placeholder="00"
value={quiz.config.info.law}
onChange={(e) =>
updateQuiz(quiz.id, (quiz) => {
quiz.config.info.law = e.target.value;
})
}
sx={{
width: "51px",
height: "48px"
}}
/>
<Typography
sx={{
fontWeight: 500,
color: theme.palette.grey3.main,
ml: "5px"
}}
>
мин.
</Typography>
</Box>
<Box
sx={{
display: "inline-flex",
alignItems: "center"
}}
>
<CustomTextField
placeholder="00"
value={quiz.config.info.law}
onChange={(e) =>
updateQuiz(quiz.id, (quiz) => {
quiz.config.info.law = e.target.value;
})
}
sx={{
width: "51px",
height: "48px"
}}
/>
<Typography
sx={{
fontWeight: 500,
color: theme.palette.grey3.main,
ml: "5px"
}}
>
сек.
</Typography>
</Box>
</Box>
</Box>
{!isSmallMonitor && <SwitchAI />} {!isSmallMonitor && <SwitchAI />}
</> </>
)} )}

@ -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 React, { useEffect, useState } from "react";
import type { InputProps, SxProps, Theme } from "@mui/material"; import type { InputProps, SxProps, Theme } from "@mui/material";
import { import {
@ -20,6 +20,9 @@ interface CustomTextFieldProps {
onChange?: (event: ChangeEvent<HTMLInputElement>) => void; onChange?: (event: ChangeEvent<HTMLInputElement>) => void;
onKeyDown?: (event: KeyboardEvent<HTMLInputElement>) => void; onKeyDown?: (event: KeyboardEvent<HTMLInputElement>) => void;
onBlur?: (event: FocusEvent<HTMLInputElement>) => void; onBlur?: (event: FocusEvent<HTMLInputElement>) => void;
onFocus?: (event: FocusEvent<HTMLInputElement>) => void;
onClick?: (event: MouseEvent<HTMLInputElement>) => void;
onPaste?: (event: ClipboardEvent<HTMLInputElement>) => void;
text?: string; text?: string;
maxLength?: number; maxLength?: number;
sx?: SxProps<Theme>; sx?: SxProps<Theme>;
@ -29,6 +32,7 @@ interface CustomTextFieldProps {
rows?: number; rows?: number;
className?: string; className?: string;
disabled?: boolean; disabled?: boolean;
inputRef?: Ref<HTMLInputElement>;
} }
export default function CustomTextField({ export default function CustomTextField({
@ -38,6 +42,9 @@ export default function CustomTextField({
onChange, onChange,
onKeyDown, onKeyDown,
onBlur, onBlur,
onFocus,
onClick,
onPaste,
text, text,
sx, sx,
error, error,
@ -49,6 +56,7 @@ export default function CustomTextField({
sxForm, sxForm,
className, className,
disabled, disabled,
inputRef,
}: CustomTextFieldProps) { }: CustomTextFieldProps) {
const theme = useTheme(); const theme = useTheme();
@ -77,8 +85,9 @@ export default function CustomTextField({
} }
}; };
const handleInputFocus = () => { const handleInputFocus = (event: React.FocusEvent<HTMLInputElement>) => {
setIsInputActive(true); setIsInputActive(true);
if (onFocus) onFocus(event);
}; };
const handleInputBlur = (event: React.FocusEvent<HTMLInputElement>) => { const handleInputBlur = (event: React.FocusEvent<HTMLInputElement>) => {
@ -117,10 +126,14 @@ export default function CustomTextField({
onFocus={handleInputFocus} onFocus={handleInputFocus}
onBlur={handleInputBlur} onBlur={handleInputBlur}
onKeyDown={onKeyDown} onKeyDown={onKeyDown}
onClick={onClick}
onPaste={onPaste}
multiline={rows > 0} multiline={rows > 0}
rows={rows} rows={rows}
disabled={disabled} disabled={disabled}
disableUnderline disableUnderline
inputRef={inputRef}
{...InputProps}
sx={{ sx={{
maxLength: maxLength, maxLength: maxLength,
borderRadius: "10px", borderRadius: "10px",

@ -16,7 +16,7 @@ const FormQuestionsPage = lazy(
const QuestionsPage = lazy(() => import("../pages/Questions/QuestionsPage")); const QuestionsPage = lazy(() => import("../pages/Questions/QuestionsPage"));
const ResultPage = lazy(() => import("../pages/ResultPage/ResultPage")); const ResultPage = lazy(() => import("../pages/ResultPage/ResultPage"));
const StartPageSettings = lazy( const StartPageSettings = lazy(
() => import("../pages/startPage/StartPageSettings"), () => import("../pages/startPage/StartPageSettings/StartPageSettings"),
); );
const StepOne = lazy(() => import("../pages/startPage/stepOne")); const StepOne = lazy(() => import("../pages/startPage/stepOne"));
const Steptwo = lazy(() => import("../pages/startPage/steptwo")); const Steptwo = lazy(() => import("../pages/startPage/steptwo"));