новый визуал таймера с логикой + визуал овертайм
This commit is contained in:
parent
9c61a9e2b8
commit
e68447dc6e
@ -1,3 +1,4 @@
|
||||
1.0.12 _ 2025-10-12 _ ютм с дизайном и беком, но без логики
|
||||
1.0.11 _ 2025-10-06 _ Merge branch 'staging'
|
||||
1.0.10 _ 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 ...
|
||||
129
src/pages/startPage/Components/OverTime.tsx
Normal file
129
src/pages/startPage/Components/OverTime.tsx
Normal file
@ -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;
|
||||
228
src/pages/startPage/Components/Timer.tsx
Normal file
228
src/pages/startPage/Components/Timer.tsx
Normal file
@ -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 { SwitchAI } from "@/ui_kit/crutchFunctionAI";
|
||||
import BackBlockedWithTooltip from "./BackBlockedWithTooltip";
|
||||
import OverTime from "../Components/OverTime";
|
||||
import Timer from "../Components/Timer";
|
||||
|
||||
const designTypes = [
|
||||
["standard", (color: string) => <LayoutStandartIcon color={color} />, "Standard"],
|
||||
@ -894,7 +896,8 @@ export default function StartPageSettings() {
|
||||
</Typography>
|
||||
</Box>
|
||||
<BackBlockedWithTooltip />
|
||||
<QuestionTimerSettings />
|
||||
<Timer/>
|
||||
<OverTime/>
|
||||
{!isSmallMonitor && <SwitchAI />}
|
||||
</>
|
||||
)}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user