новый визуал таймера с логикой + визуал овертайм

This commit is contained in:
Nastya 2025-10-12 22:23:11 +03:00
parent 9c61a9e2b8
commit e68447dc6e
5 changed files with 362 additions and 13 deletions

@ -1,3 +1,4 @@
1.0.12 _ 2025-10-12 _ ютм с дизайном и беком, но без логики
1.0.11 _ 2025-10-06 _ Merge branch 'staging' 1.0.11 _ 2025-10-06 _ Merge branch 'staging'
1.0.10 _ 2025-10-05 _ utm 1.0.10 _ 2025-10-05 _ utm
1.0.9 _ 2025-10-05 _ utm 1.0.9 _ 2025-10-05 _ utm

@ -1,12 +0,0 @@
import { useAuthRedirect } from "../../utils/hooks/useAuthRedirect";
export default function Payment() {
// Используем хук авторизации
const { isProcessing } = useAuthRedirect();
// Если идет обработка авторизации, показываем загрузку
if (isProcessing) {
return <div>Идёт загрузка...</div>;
}
// ... existing component code ...

@ -0,0 +1,129 @@
import { useCurrentQuiz } from "@/stores/quizes/hooks";
import CustomTextField from "@/ui_kit/CustomTextField"
import { Box, Typography, useTheme } from "@mui/material"
import CustomizedSwitch from "@ui_kit/CustomSwitch";
const OverTime = () => {
const theme = useTheme();
const quiz = useCurrentQuiz();
if (!quiz) return null;
return (
<Box sx={{ display: "flex", flexDirection: "column", mt: "20px", padding: "15px", backgroundColor: "#F2F3F7", width: "100%", maxWidth: "357px", height: "283px", borderRadius: "8px" }}>
<Box sx={{ display: "inline-flex", alignItems: "center", height: "31px", gap: "20px" }}>
<CustomizedSwitch/>
<Typography
sx={{
fontWeight: 500,
color: theme.palette.grey3.main,
fontSize: "16px",
}}
>
Включить счётчик
</Typography>
</Box>
<Box sx={{ mt: "25px" }}>
<Typography
sx={{
fontWeight: 500,
color: theme.palette.grey3.main,
fontSize: "16px",
}}
>
Введите описание
</Typography>
<CustomTextField sx={{ height: "48px", width: "100%", maxWidth: "327px", mt: "9px" }}
placeholder="Квиз будет недоступен через:"
/>
<Box sx={{ display: "flex", flexDirection: "row-reverse", height: "16px" }}>
<Typography
sx={{
fontWeight: 400,
color: theme.palette.grey3.main,
fontSize: "12px",
}}
>
0/40
</Typography>
</Box>
</Box>
<Box sx={{ mt: "2px" }}>
<Typography
sx={{
fontWeight: 400,
color: theme.palette.grey3.main,
fontSize: "16px",
}}
>
Введите время
</Typography>
<Box sx={{ display: "inline-flex", gap: "10px", pt: "8px" }}>
<Box sx={{ display: "flex", flexDirection: "column", alignItems: "center", gap: "3px" }}>
<CustomTextField sx={{ height: "48px", width: "100%", maxWidth: "51px" }}
placeholder="00"
/>
<Typography
sx={{
fontWeight: 400,
color: theme.palette.grey3.main,
fontSize: "16px",
mt: "3px"
}}
>
дней
</Typography>
</Box>
<Box sx={{ display: "flex", flexDirection: "column", alignItems: "center", gap: "3px" }}>
<CustomTextField sx={{ height: "48px", width: "100%", maxWidth: "51px" }}
placeholder="00"
/>
<Typography
sx={{
fontWeight: 400,
color: theme.palette.grey3.main,
fontSize: "16px",
mt: "3px"
}}
>
часов
</Typography>
</Box>
<Box sx={{ display: "flex", flexDirection: "column", alignItems: "center", gap: "3px" }}>
<CustomTextField sx={{ height: "48px", width: "100%", maxWidth: "51px" }}
placeholder="00"
/>
<Typography
sx={{
fontWeight: 400,
color: theme.palette.grey3.main,
fontSize: "16px",
mt: "3px"
}}
>
мин.
</Typography>
</Box>
<Box sx={{ display: "flex", flexDirection: "column", alignItems: "center", gap: "3px" }}>
<CustomTextField sx={{ height: "48px", width: "100%", maxWidth: "51px" }}
placeholder="00"
/>
<Typography
sx={{
fontWeight: 400,
color: theme.palette.grey3.main,
fontSize: "16px",
mt: "3px"
}}
>
сек.
</Typography>
</Box>
</Box>
</Box>
</Box>
)
}
export default OverTime;

@ -0,0 +1,228 @@
import { updateQuiz } from "@/stores/quizes/actions";
import { useCurrentQuiz } from "@/stores/quizes/hooks";
import CustomTextField from "@/ui_kit/CustomTextField"
import { Box, Typography, useMediaQuery, useTheme } from "@mui/material"
import moment from "moment";
import { useMemo, useRef, useState } from "react";
import CustomizedSwitch from "@ui_kit/CustomSwitch";
const Timer = () => {
const theme = useTheme();
const quiz = useCurrentQuiz();
const isMobile = useMediaQuery(theme.breakpoints.down(650));
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 as any)?.config?.time_of_passing ?? 0;
const sec = Number(raw) || 0;
return Math.max(0, sec);
}, [(quiz as any)?.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 handleMinutesKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
if (allowControlKey(e)) return;
const isDigit = /^[0-9]$/.test(e.key);
if (!isDigit) {
e.preventDefault();
return;
}
const target = e.currentTarget;
console.log("target")
console.log(target)
const selectionStart = target.selectionStart ?? 0;
const selectionEnd = target.selectionEnd ?? 0;
const selectionLength = Math.max(0, selectionEnd - selectionStart);
const currentLength = toDigits(minutes).length;
if (selectionLength === 0 && currentLength >= 2) {
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();
if ((e.target as HTMLInputElement).value.length >= 2) return;
const text = e.clipboardData.getData("text");
const next = clampTwoDigits(text);
setSeconds(next);
persist(minutes, next);
};
if (!quiz) return null;
return (
<Box sx={{ display: "flex", flexDirection: "column", gap: "25px", mt: "20px", padding: "15px", backgroundColor: "#F2F3F7", width: "100%", maxWidth: "386px", height: "187px", borderRadius: "8px" }}>
<Box sx={{ display: "inline-flex", alignItems: "center", height: "31px", 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,
fontSize: "16px",
}}
>
Включить таймер вопросов
</Typography>
</Box>
<Box>
<Typography
sx={{
fontWeight: 500,
color: theme.palette.grey3.main,
fontSize: "16px",
}}
>
Введите время
</Typography>
<Box sx={{ display: "inline-flex", gap: "10px", pt: "8px" }}>
<Box sx={{ display: "flex", flexDirection: "column", alignItems: "center" }}>
<CustomTextField
id="question-timer-minutes"
placeholder="00"
type="tel"
value={minutes}
disabled={!enabled}
onChange={(e) => {
const next = clampTwoDigits(e.target.value).slice(0, 2);
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={handleMinutesKeyDown as any}
onPaste={handleMinutesPaste as any}
InputProps={{ inputProps: { pattern: "\\d*", inputMode: "numeric", maxLength: 2 } }}
sx={{ height: "48px", width: "100%", maxWidth: "51px" }}
inputRef={minutesRef}
/>
<Typography
sx={{
fontWeight: 400,
color: theme.palette.grey3.main,
fontSize: "16px",
mt: "4px"
}}
>
мин.
</Typography>
</Box>
<Box sx={{ display: "flex", flexDirection: "column", 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", maxLength: 2 } }}
sx={{ height: "48px", width: "100%", maxWidth: "51px" }}
inputRef={secondsRef}
/>
<Typography
sx={{
fontWeight: 400,
color: theme.palette.grey3.main,
fontSize: "16px",
mt: "4px"
}}
>
сек.
</Typography>
</Box>
</Box>
</Box>
</Box>
)
}
export default Timer;

@ -48,6 +48,8 @@ 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"; import BackBlockedWithTooltip from "./BackBlockedWithTooltip";
import OverTime from "../Components/OverTime";
import Timer from "../Components/Timer";
const designTypes = [ const designTypes = [
["standard", (color: string) => <LayoutStandartIcon color={color} />, "Standard"], ["standard", (color: string) => <LayoutStandartIcon color={color} />, "Standard"],
@ -894,7 +896,8 @@ export default function StartPageSettings() {
</Typography> </Typography>
</Box> </Box>
<BackBlockedWithTooltip /> <BackBlockedWithTooltip />
<QuestionTimerSettings /> <Timer/>
<OverTime/>
{!isSmallMonitor && <SwitchAI />} {!isSmallMonitor && <SwitchAI />}
</> </>
)} )}