replace quiz preview with QuizAnswerer

This commit is contained in:
nflnkr 2024-04-16 22:31:51 +03:00
parent d4d252ce71
commit ac0ca414ec
7 changed files with 45 additions and 334 deletions

@ -7,7 +7,7 @@
"@emotion/react": "^11.10.5",
"@emotion/styled": "^11.10.5",
"@frontend/kitui": "^1.0.74",
"@frontend/squzanswerer": "^1.0.22",
"@frontend/squzanswerer": "^1.0.36",
"@mui/icons-material": "^5.10.14",
"@mui/material": "^5.10.14",
"@mui/x-charts": "^6.19.5",

@ -70,6 +70,7 @@ export type QuizTheme =
| "Design10";
export interface QuizConfig {
spec: undefined | true;
type: QuizType;
noStartPage: boolean;
startpageType: QuizStartpageType;
@ -140,6 +141,7 @@ export type FieldSettingsDrawerState = {
};
export const defaultQuizConfig: QuizConfig = {
spec: undefined,
type: null,
noStartPage: false,
startpageType: null,

@ -78,6 +78,7 @@ export default function ViewPublicationPage() {
rep: quiz.repeatable,
cfg: quiz.config,
},
show_badge: true,
}}
quizId={quizId}
preview

@ -3,14 +3,12 @@ import { devtools } from "zustand/middleware";
interface QuizPreviewStore {
isPreviewShown: boolean;
currentQuestionIndex: number;
}
export const useQuizPreviewStore = create<QuizPreviewStore>()(
devtools(
(set, get) => ({
isPreviewShown: false,
currentQuestionIndex: 0,
}),
{
name: "quizPreview",
@ -29,18 +27,3 @@ export const toggleQuizPreview = () =>
useQuizPreviewStore.setState((state) => ({
isPreviewShown: !state.isPreviewShown,
}));
export const setCurrentQuestionIndex = (step: number) =>
useQuizPreviewStore.setState((state) => ({
currentQuestionIndex: (state.currentQuestionIndex = step),
}));
export const incrementCurrentQuestionIndex = (maxStep: number) =>
useQuizPreviewStore.setState((state) => ({
currentQuestionIndex: Math.min(state.currentQuestionIndex + 1, maxStep),
}));
export const decrementCurrentQuestionIndex = () =>
useQuizPreviewStore.setState((state) => ({
currentQuestionIndex: Math.max(state.currentQuestionIndex - 1, 0),
}));

@ -1,16 +1,13 @@
import { Box, IconButton, ThemeProvider } from "@mui/material";
import { toggleQuizPreview, useQuizPreviewStore } from "@root/quizPreview";
import { QuizAnswerer } from "@frontend/squzanswerer";
import ResizeIcon from "@icons/ResizeIcon";
import { AnyTypedQuizQuestion } from "@model/questionTypes/shared";
import { Box, ThemeProvider } from "@mui/material";
import { useQuestionsStore } from "@root/questions/store";
import { useQuizPreviewStore } from "@root/quizPreview";
import { useCurrentQuiz } from "@root/quizes/hooks";
import { themesPublication } from "@utils/themes/Publication/themePublication";
import { useLayoutEffect, useRef } from "react";
import { Rnd } from "react-rnd";
import { useWindowSize } from "../../utils/hooks/useWindowSize";
import QuizPreviewLayout from "./QuizPreviewLayout";
import ResizeIcon from "@icons/ResizeIcon";
import { themesPublication } from "@utils/themes/Publication/themePublication";
import { useCurrentQuiz } from "@root/quizes/hooks";
const DRAG_PARENT_MARGIN = 0;
const NAVBAR_HEIGHT = 0;
const DRAG_PARENT_BOTTOM_MARGIN = 0;
interface RndPositionAndSize {
x: number;
@ -23,6 +20,9 @@ export default function QuizPreview() {
const isPreviewShown = useQuizPreviewStore((state) => state.isPreviewShown);
const rndParentRef = useRef<HTMLDivElement>(null);
const quiz = useCurrentQuiz();
const questions = useQuestionsStore((state) => state.questions).filter(
(q): q is AnyTypedQuizQuestion => q.type !== null,
);
const rndRef = useRef<Rnd | null>(null);
const rndPositionAndSizeRef = useRef<RndPositionAndSize>({
x: 0,
@ -32,6 +32,8 @@ export default function QuizPreview() {
});
const isFirstShowRef = useRef<boolean>(true);
if (!quiz) return null;
useLayoutEffect(
function stickPreviewToBottomRight() {
const rnd = rndRef.current;
@ -68,8 +70,8 @@ export default function QuizPreview() {
data-cy="quiz-preview-container"
sx={{
position: "fixed",
top: NAVBAR_HEIGHT + DRAG_PARENT_MARGIN,
left: DRAG_PARENT_MARGIN,
top: 0,
left: 0,
bottom: 70,
right: 70,
// backgroundColor: "rgba(0, 100, 0, 0.2)",
@ -119,10 +121,31 @@ export default function QuizPreview() {
overflow: "hidden",
pointerEvents: "auto",
boxShadow: "0px 5px 10px 2px rgba(34, 60, 80, 0.2)",
backgroundColor: "white",
}}
cancel=".cancel"
>
<QuizPreviewLayout />
<QuizAnswerer
className="quiz-preview-draghandle"
quizSettings={{
cnt: questions.length,
questions,
recentlyCompleted: false,
settings: {
fp: quiz.fingerprinting,
delay: 0,
due: quiz.due_to,
lim: quiz.limit,
name: quiz.name,
pausable: quiz.pausable,
rep: quiz.repeatable,
cfg: quiz.config,
},
show_badge: true,
}}
quizId={quiz.qid}
preview
/>
</Rnd>
)}
</Box>

@ -1,298 +0,0 @@
import {
Box,
Button,
LinearProgress,
Paper,
Typography,
FormControl,
Select as MuiSelect,
MenuItem,
useTheme,
} from "@mui/material";
import { useQuestionsStore } from "@root/questions/store";
import {
decrementCurrentQuestionIndex,
incrementCurrentQuestionIndex,
useQuizPreviewStore,
setCurrentQuestionIndex,
} from "@root/quizPreview";
import {
AnyTypedQuizQuestion,
UntypedQuizQuestion,
} from "model/questionTypes/shared";
import { useEffect, useRef, useState } from "react";
import ArrowLeft from "../../assets/icons/questionsPage/arrowLeft";
import Date from "./QuizPreviewQuestionTypes/Date";
import Emoji from "./QuizPreviewQuestionTypes/Emoji";
import File from "./QuizPreviewQuestionTypes/File";
import Images from "./QuizPreviewQuestionTypes/Images";
import Number from "./QuizPreviewQuestionTypes/Number";
import Page from "./QuizPreviewQuestionTypes/Page";
import Rating from "./QuizPreviewQuestionTypes/Rating";
import Select, { ArrowDownTheme } from "./QuizPreviewQuestionTypes/Select";
import Text from "./QuizPreviewQuestionTypes/Text";
import Variant from "./QuizPreviewQuestionTypes/Variant";
import Varimg from "./QuizPreviewQuestionTypes/Varimg";
import { notReachable } from "../../utils/notReachable";
import ArrowDownIcon from "@icons/ArrowDownIcon";
export default function QuizPreviewLayout() {
const theme = useTheme();
const questions = useQuestionsStore((state) => state.questions);
const currentQuizStep = useQuizPreviewStore(
(state) => state.currentQuestionIndex,
);
const [widthPreview, setWidthPreview] = useState(null);
const nonDeletedQuizQuestions = questions.filter(
(question) => !question.deleted && question.type !== "result",
);
const maxCurrentQuizStep =
nonDeletedQuizQuestions.length > 0 ? nonDeletedQuizQuestions.length - 1 : 0;
const currentProgress = Math.floor(
(currentQuizStep / maxCurrentQuizStep) * 100,
);
const PreviewWin = useRef(0);
const currentQuestion = nonDeletedQuizQuestions[currentQuizStep];
useEffect(
function resetCurrentQuizStep() {
if (currentQuizStep > maxCurrentQuizStep) {
decrementCurrentQuestionIndex();
}
},
[currentQuizStep, maxCurrentQuizStep],
);
const observer = useRef(
new ResizeObserver((entries) => {
const { width } = entries[0].contentRect;
setWidthPreview(width);
}),
);
useEffect(() => {
observer.current.observe(PreviewWin.current);
}, [PreviewWin, observer]);
return (
<Paper
ref={PreviewWin}
className="quiz-preview-draghandle"
data-cy="quiz-preview-layout"
sx={{
height: "100%",
display: "flex",
flexDirection: "column",
flexGrow: 1,
borderRadius: "12px",
pointerEvents: "auto",
backgroundColor: theme.palette.background.default,
}}
>
<Box
sx={{
p: "40px 20px 20px",
whiteSpace: "break-spaces",
overflowY: "auto",
flexGrow: 1,
"&::-webkit-scrollbar": { width: 0, display: "none" },
msOverflowStyle: "none",
scrollbarWidth: "none",
}}
>
<QuestionPreviewComponent
question={currentQuestion}
widthPreview={widthPreview}
/>
</Box>
<Box
sx={{
mt: "auto",
p: "16px",
borderTop: "1px solid #E3E3E3",
}}
>
<Box sx={{ marginBottom: "10px" }}>
<FormControl
fullWidth
size="small"
sx={{ width: "100%", minWidth: "200px", height: "48px" }}
className="cancel"
>
<MuiSelect
id="category-select"
variant="outlined"
value={currentQuizStep}
placeholder="Заголовок вопроса"
onChange={({ target }) =>
setCurrentQuestionIndex(window.Number(target.value))
}
sx={{
height: "48px",
borderRadius: "8px",
"& .MuiOutlinedInput-notchedOutline": {
border: `1px solid ${theme.palette.primary.main} !important`,
},
"& .MuiSelect-icon": {
color: theme.palette.primary.main,
},
}}
MenuProps={{
PaperProps: {
sx: {
mt: "8px",
p: "4px",
borderRadius: "8px",
border: "1px solid #EEE4FC",
boxShadow: "0px 8px 24px rgba(210, 208, 225, 0.4)",
backgroundColor: theme.palette.background.default,
},
},
MenuListProps: {
sx: {
py: 0,
display: "flex",
flexDirection: "column",
gap: "8px",
maxWidth: "330px",
"& .Mui-selected": {
backgroundColor: theme.palette.background.default,
color: theme.palette.primary.main,
},
},
},
}}
inputProps={{
sx: {
color: theme.palette.primary.main,
display: "block",
px: "9px",
gap: "20px",
width: "87%",
overflow: "hidden",
textOverflow: "ellipsis",
},
}}
IconComponent={ArrowDownTheme}
>
{Object.values(questions.filter((q) => q.type !== "result")).map(
({ id, title }, index) => (
<MenuItem
key={id}
value={index}
sx={{
display: "flex",
alignItems: "center",
gap: "20px",
p: "4px",
borderRadius: "5px",
color: "#9A9AAF",
wordBreak: "break-word",
whiteSpace: "normal",
}}
>
{`${index + 1}. ${title}`}
</MenuItem>
),
)}
</MuiSelect>
</FormControl>
</Box>
<Box sx={{ display: "flex", alignItems: "center" }}>
<Box
sx={{
flexGrow: 1,
display: "flex",
flexDirection: "column",
gap: 1,
}}
>
<Typography>
{nonDeletedQuizQuestions.length > 0
? `Вопрос ${currentQuizStep + 1} из ${
nonDeletedQuizQuestions.length
}`
: "Нет вопросов"}
</Typography>
{nonDeletedQuizQuestions.length > 0 && (
<LinearProgress
variant="determinate"
value={currentProgress}
sx={{
"&.MuiLinearProgress-colorPrimary": {
backgroundColor: "fadePurple.main",
},
"& .MuiLinearProgress-barColorPrimary": {
backgroundColor: "brightPurple.main",
},
}}
/>
)}
</Box>
<Box
sx={{
ml: 2,
display: "flex",
gap: 1,
}}
>
<Button
variant="outlined"
onClick={decrementCurrentQuestionIndex}
disabled={currentQuizStep === 0}
sx={{ px: 1, minWidth: 0 }}
className="cancel"
>
<ArrowLeft color={theme.palette.primary.main} />
</Button>
<Button
variant="contained"
onClick={() => incrementCurrentQuestionIndex(maxCurrentQuizStep)}
disabled={currentQuizStep >= maxCurrentQuizStep}
className="cancel"
>
Далее
</Button>
</Box>
</Box>
</Box>
</Paper>
);
}
function QuestionPreviewComponent({
question,
widthPreview,
}: {
question: AnyTypedQuizQuestion | UntypedQuizQuestion | undefined;
widthPreview?: number;
}) {
if (!question || question.type === null) return null;
switch (question.type) {
case "variant":
return <Variant question={question} widthPreview={widthPreview} />;
case "images":
return <Images question={question} widthPreview={widthPreview} />;
case "varimg":
return <Varimg question={question} widthPreview={widthPreview} />;
case "emoji":
return <Emoji question={question} widthPreview={widthPreview} />;
case "text":
return <Text question={question} widthPreview={widthPreview} />;
case "select":
return <Select question={question} widthPreview={widthPreview} />;
case "date":
return <Date question={question} widthPreview={widthPreview} />;
case "number":
return <Number question={question} widthPreview={widthPreview} />;
case "file":
return <File question={question} widthPreview={widthPreview} />;
case "page":
return <Page question={question} widthPreview={widthPreview} />;
case "rating":
return <Rating question={question} widthPreview={widthPreview} />;
default:
notReachable(question);
}
}

@ -1517,10 +1517,10 @@
immer "^10.0.2"
reconnecting-eventsource "^1.6.2"
"@frontend/squzanswerer@^1.0.22":
version "1.0.22"
resolved "https://penahub.gitlab.yandexcloud.net/api/v4/projects/43/packages/npm/@frontend/squzanswerer/-/@frontend/squzanswerer-1.0.22.tgz#a4ae7f6d10b489c819e7197fe06a08e25ffa31be"
integrity sha1-pK5/bRC0icgZ5xl/4GoI4l/6Mb4=
"@frontend/squzanswerer@^1.0.36":
version "1.0.36"
resolved "https://penahub.gitlab.yandexcloud.net/api/v4/projects/43/packages/npm/@frontend/squzanswerer/-/@frontend/squzanswerer-1.0.36.tgz#778be1bdf3eca014319881a71da8b32f7fd77546"
integrity sha1-d4vhvfPsoBQxmIGnHaizL3/XdUY=
dependencies:
bowser "1.9.4"
country-flag-emoji-polyfill "^0.1.8"