add question previews

This commit is contained in:
nflnkr 2023-10-09 15:33:45 +03:00
parent 650ddd7129
commit a06c5c2010
21 changed files with 948 additions and 120 deletions

@ -8,6 +8,7 @@
"@emotion/styled": "^11.10.5",
"@mui/icons-material": "^5.10.14",
"@mui/material": "^5.10.14",
"@mui/x-date-pickers": "^6.16.1",
"@testing-library/jest-dom": "^5.14.1",
"@testing-library/react": "^13.0.0",
"@testing-library/user-event": "^13.2.1",
@ -17,6 +18,7 @@
"@types/react": "^18.0.0",
"@types/react-dnd": "^3.0.2",
"@types/react-dom": "^18.0.0",
"dayjs": "^1.11.10",
"emoji-mart": "^5.5.2",
"file-saver": "^2.0.5",
"html-to-image": "^1.11.11",

@ -0,0 +1,39 @@
import { Box } from "@mui/material";
export default function CalendarIcon() {
return (
<Box
sx={{
display: "flex",
alignItems: "center",
justifyContent: "center",
flexShrink: 0,
"&:hover path": {
stroke: "#581CA7",
},
"&:active path": {
stroke: "#FB5607",
},
"&:hover rect": {
stroke: "#581CA7",
},
"&:active rect": {
stroke: "#FB5607",
},
}}
>
<svg width="20" height="22" viewBox="0 0 20 22" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect x="1" y="2.5" width="18" height="18" rx="5" stroke="#7E2AEA" strokeWidth="1.5" />
<path d="M1 7.5H19" stroke="#7E2AEA" strokeWidth="1.5" strokeLinejoin="round" />
<path d="M14.5 1L14.5 4" stroke="#7E2AEA" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
<path d="M5.5 1L5.5 4" stroke="#7E2AEA" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
<path d="M4.5 11.5H5.5" stroke="#7E2AEA" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
<path d="M9.5 11.5H10.5" stroke="#7E2AEA" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
<path d="M14.5 11.5H15.5" stroke="#7E2AEA" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
<path d="M4.5 15.5H5.5" stroke="#7E2AEA" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
<path d="M9.5 15.5H10.5" stroke="#7E2AEA" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
<path d="M14.5 15.5H15.5" stroke="#7E2AEA" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
</svg>
</Box>
);
}

@ -6,7 +6,6 @@ import "./index.css";
import { BrowserRouter, Route, Routes } from "react-router-dom";
import lightTheme from "./utils/themes/light";
import { ThemeProvider } from "@mui/material";
import StartPage from "./pages/startPage/StartPage";
import Main from "./pages/main";
import QuestionsPage from "./pages/Questions/QuestionsPage";
@ -16,6 +15,16 @@ import { Result } from "./pages/Result/Result";
import { Setting } from "./pages/Result/Setting";
import MyQuizzesFull from "./pages/createQuize/MyQuizzesFull";
import ImageCrop from "@ui_kit/Modal/ImageCrop";
import { LocalizationProvider } from "@mui/x-date-pickers";
import { AdapterDayjs } from "@mui/x-date-pickers/AdapterDayjs";
import "dayjs/locale/ru";
import dayjs from "dayjs";
import { ruRU } from '@mui/x-date-pickers/locales';
dayjs.locale("ru");
const localeText = ruRU.components.MuiLocalizationProvider.defaultProps.localeText;
const routeslink: {
path: string;
@ -40,6 +49,7 @@ const root = createRoot(document.getElementById("root")!);
root.render(
<DndProvider backend={HTML5Backend}>
<LocalizationProvider dateAdapter={AdapterDayjs} adapterLocale="ru" localeText={localeText}>
<ThemeProvider theme={lightTheme}>
<BrowserRouter>
<Routes>
@ -51,5 +61,6 @@ root.render(
</Routes>
</BrowserRouter>
</ThemeProvider>
</LocalizationProvider>
</DndProvider>
);

@ -4,14 +4,14 @@ import { devtools } from "zustand/middleware";
interface QuizPreviewStore {
isPreviewShown: boolean;
currentQuizStep: number;
currentQuestionIndex: number;
}
export const useQuizPreviewStore = create<QuizPreviewStore>()(
devtools(
(set, get) => ({
isPreviewShown: true,
currentQuizStep: 0,
currentQuestionIndex: 0,
}),
{
name: "quizPreview",
@ -28,10 +28,10 @@ export const toggleQuizPreview = () => useQuizPreviewStore.setState(
state => ({ isPreviewShown: !state.isPreviewShown })
);
export const incrementCurrentQuizStep = (maxStep: number) => useQuizPreviewStore.setState(
state => ({ currentQuizStep: Math.min(state.currentQuizStep + 1, maxStep) })
export const incrementCurrentQuestionIndex = (maxStep: number) => useQuizPreviewStore.setState(
state => ({ currentQuestionIndex: Math.min(state.currentQuestionIndex + 1, maxStep) })
);
export const decrementCurrentQuizStep = () => useQuizPreviewStore.setState(
state => ({ currentQuizStep: Math.max(state.currentQuizStep - 1, 0) })
export const decrementCurrentQuestionIndex = () => useQuizPreviewStore.setState(
state => ({ currentQuestionIndex: Math.max(state.currentQuestionIndex - 1, 0) })
);

@ -0,0 +1,61 @@
import { Slider } from "@mui/material";
type CustomSliderProps = {
defaultValue?: number;
value?: number;
min?: number;
max?: number;
step?: number;
onChange?: (value: number | number[]) => void;
};
export const CustomSlider = ({
defaultValue,
value,
min = 0,
max = 100,
step,
onChange,
}: CustomSliderProps) => {
const handleChange = ({ type }: Event, newValue: number | number[]) => {
// Для корректной работы слайдера в FireFox
if (type !== "change") {
onChange?.(newValue);
}
};
return (
<Slider
value={value}
defaultValue={defaultValue}
min={min}
max={max}
step={step}
onChange={handleChange}
valueLabelDisplay="auto"
sx={{
color: "#7E2AEA",
height: "12px",
"& .MuiSlider-track": {
border: "none",
},
"& .MuiSlider-rail": {
backgroundColor: "#F2F3F7",
border: `1px solid #9A9AAF`,
},
"& .MuiSlider-thumb": {
height: 32,
width: 32,
border: `6px solid "#7E2AEA`,
backgroundColor: "white",
boxShadow: `0px 0px 0px 3px white,
0px 4px 4px 3px #C3C8DD`,
"&:focus, &:hover, &.Mui-active, &.Mui-focusVisible": {
boxShadow: `0px 0px 0px 3px white,
0px 4px 4px 3px #C3C8DD`,
},
},
}}
/>
);
};

@ -0,0 +1,68 @@
import CalendarIcon from "@icons/CalendarIcon";
import { Typography, Box, SxProps, Theme, useMediaQuery, useTheme } from "@mui/material";
import { DatePicker } from "@mui/x-date-pickers";
import { Dayjs } from "dayjs";
interface Props {
label?: string;
sx?: SxProps<Theme>;
value?: Dayjs | null;
onChange?: (value: Dayjs | null) => void;
}
export default function LabeledDatePicker({ label, value, onChange, sx }: Props) {
const theme = useTheme();
const upLg = useMediaQuery(theme.breakpoints.up("md"));
return (
<Box
sx={{
...sx,
}}
>
{label && (
<Typography
sx={{
fontWeight: 500,
fontSize: "16px",
lineHeight: "20px",
color: "#4D4D4D",
mb: "10px",
}}
>
{label}
</Typography>
)}
<DatePicker
value={value}
onChange={onChange}
slots={{
openPickerIcon: () => <CalendarIcon />,
}}
slotProps={{
openPickerButton: {
sx: {
p: 0,
},
},
}}
sx={{
"& .MuiInputBase-root": {
backgroundColor: "#F2F3F7",
borderRadius: "10px",
pr: "22px",
"& input": {
py: "11px",
pl: upLg ? "20px" : "13px",
lineHeight: "19px",
},
"& fieldset": {
borderColor: "#9A9AAF",
},
},
}}
/>
</Box>
);
}

@ -3,7 +3,7 @@ import { Box, IconButton } from "@mui/material";
import { toggleQuizPreview, useQuizPreviewStore } from "@root/quizPreview";
import { useLayoutEffect, useRef } from "react";
import { Rnd } from "react-rnd";
import QuizPreviewContent from "./QuizPreviewContent";
import QuizPreviewLayout from "./QuizPreviewLayout";
import ResizeIcon from "./ResizeIcon";
import VisibilityIcon from '@mui/icons-material/Visibility';
@ -23,7 +23,7 @@ export default function QuizPreview() {
const isPreviewShown = useQuizPreviewStore(state => state.isPreviewShown);
const rndParentRef = useRef<HTMLDivElement>(null);
const rndRef = useRef<Rnd | null>(null);
const rndPositionAndSizeRef = useRef<RndPositionAndSize>({ x: 0, y: 0, width: "0", height: "0" });
const rndPositionAndSizeRef = useRef<RndPositionAndSize>({ x: 0, y: 0, width: "340", height: "480" });
const isFirstShowRef = useRef<boolean>(true);
useLayoutEffect(function stickPreviewToBottomRight() {
@ -60,8 +60,8 @@ export default function QuizPreview() {
>
{isPreviewShown &&
<Rnd
minHeight={480}
minWidth={300}
minHeight={300}
minWidth={340}
bounds="parent"
ref={rndRef}
dragHandleClassName="quiz-preview-draghandle"
@ -100,7 +100,7 @@ export default function QuizPreview() {
pointerEvents: "auto",
}}
>
<QuizPreviewContent />
<QuizPreviewLayout />
<IconButton
className="quiz-preview-draghandle"
sx={{

@ -1,79 +0,0 @@
import { Box, Button, LinearProgress, Paper } from "@mui/material";
import { Question, questionStore } from "@root/questions";
import { decrementCurrentQuizStep, incrementCurrentQuizStep, useQuizPreviewStore } from "@root/quizPreview";
import { useParams } from "react-router-dom";
import ArrowLeft from "../../assets/icons/questionsPage/arrowLeft";
export default function QuizPreviewContent() {
const quizId = useParams().quizId ?? 0;
const listQuestions = questionStore(state => state.listQuestions);
const currentQuizStep = useQuizPreviewStore(state => state.currentQuizStep);
const quizQuestions: Question[] | undefined = listQuestions[quizId];
const currentProgress = Math.floor((currentQuizStep / quizQuestions.length) * 100);
const currentQuestion = quizQuestions[currentQuizStep];
return (
<Paper sx={{
height: "100%",
display: "flex",
flexDirection: "column",
flexGrow: 1,
borderRadius: "12px",
pointerEvents: "auto",
}}>
<Box sx={{
p: "16px",
whiteSpace: "break-spaces",
overflowY: "auto",
flexGrow: 1,
}}>
</Box>
<Box sx={{
mt: "auto",
p: "16px",
display: "flex",
borderTop: "1px solid #E3E3E3",
alignItems: "center",
}}>
<Box sx={{
flexGrow: 1,
}}>
<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={decrementCurrentQuizStep}
disabled={currentQuizStep === 0}
sx={{ px: 1, minWidth: 0 }}
>
<ArrowLeft />
</Button>
<Button
variant="contained"
onClick={() => incrementCurrentQuizStep(quizQuestions.length)}
disabled={currentQuizStep === quizQuestions.length}
>Далее</Button>
</Box>
</Box>
</Paper>
);
}

@ -0,0 +1,133 @@
import { Box, Button, LinearProgress, Paper, Typography } from "@mui/material";
import { Question, questionStore } from "@root/questions";
import { decrementCurrentQuestionIndex, incrementCurrentQuestionIndex, useQuizPreviewStore } from "@root/quizPreview";
import { useParams } from "react-router-dom";
import ArrowLeft from "../../assets/icons/questionsPage/arrowLeft";
import Variant from "./QuizPreviewQuestionTypes/Variant";
import { FC, useEffect } from "react";
import Images from "./QuizPreviewQuestionTypes/Images";
import Varimg from "./QuizPreviewQuestionTypes/Varimg";
import Emoji from "./QuizPreviewQuestionTypes/Emoji";
import Text from "./QuizPreviewQuestionTypes/Text";
import Select from "./QuizPreviewQuestionTypes/Select";
import Date from "./QuizPreviewQuestionTypes/Date";
import Number from "./QuizPreviewQuestionTypes/Number";
import File from "./QuizPreviewQuestionTypes/File";
import Page from "./QuizPreviewQuestionTypes/Page";
import Rating from "./QuizPreviewQuestionTypes/Rating";
type QuizQuestionType = "variant" | "images" | "varimg" | "emoji" | "text" | "select" | "date" | "number" | "file" | "page" | "rating";
const QuestionPreviewComponentByType: Record<QuizQuestionType, FC<{ question: Question; }>> = {
variant: Variant,
images: Images,
varimg: Varimg,
emoji: Emoji,
text: Text,
select: Select,
date: Date,
number: Number,
file: File,
page: Page,
rating: Rating,
};
export default function QuizPreviewLayout() {
const quizId = useParams().quizId ?? 0;
const listQuestions = questionStore(state => state.listQuestions);
const currentQuizStep = useQuizPreviewStore(state => state.currentQuestionIndex);
const quizQuestions: Question[] | undefined = listQuestions[quizId];
const maxCurrentQuizStep = quizQuestions?.length > 0 ? quizQuestions.length - 1 : 0;
const currentProgress = Math.floor((currentQuizStep / maxCurrentQuizStep) * 100);
const currentQuestion = quizQuestions[currentQuizStep];
const QuestionComponent = currentQuestion
? QuestionPreviewComponentByType[currentQuestion.type as QuizQuestionType]
: null;
const questionElement = QuestionComponent
? <QuestionComponent key={currentQuestion.id} question={currentQuestion} />
: null;
useEffect(function resetCurrentQuizStep() {
if (currentQuizStep > maxCurrentQuizStep) {
decrementCurrentQuestionIndex();
}
}, [currentQuizStep, maxCurrentQuizStep]);
return (
<Paper sx={{
height: "100%",
display: "flex",
flexDirection: "column",
flexGrow: 1,
borderRadius: "12px",
pointerEvents: "auto",
}}>
<Box sx={{
p: "16px",
whiteSpace: "break-spaces",
overflowY: "auto",
flexGrow: 1,
}}>
{questionElement}
</Box>
<Box sx={{
mt: "auto",
p: "16px",
display: "flex",
borderTop: "1px solid #E3E3E3",
alignItems: "center",
}}>
<Box sx={{
flexGrow: 1,
display: "flex",
flexDirection: "column",
gap: 1,
}}>
<Typography>
{quizQuestions.length > 0
? `Вопрос ${currentQuizStep + 1} из ${quizQuestions.length}`
: "Нет вопросов"
}
</Typography>
{quizQuestions.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 }}
>
<ArrowLeft />
</Button>
<Button
variant="contained"
onClick={() => incrementCurrentQuestionIndex(maxCurrentQuizStep)}
disabled={currentQuizStep >= maxCurrentQuizStep}
>Далее</Button>
</Box>
</Box>
</Paper>
);
}

@ -0,0 +1,22 @@
import { Box, Typography } from "@mui/material";
import { Question } from "@root/questions";
import LabeledDatePicker from "@ui_kit/LabeledDatePicker";
interface Props {
question: Question;
}
export default function Date({ question }: Props) {
return (
<Box sx={{
display: "flex",
flexDirection: "column",
gap: 1,
}}>
<Typography variant="h6">{question.title}</Typography>
<LabeledDatePicker />
</Box>
);
}

@ -0,0 +1,39 @@
import InfoIcon from "@icons/InfoIcon";
import { Box, FormControl, FormControlLabel, FormLabel, Radio, RadioGroup, Tooltip, Typography } from "@mui/material";
import { Question } from "@root/questions";
import { useState, ChangeEvent } from "react";
interface Props {
question: Question;
}
export default function Emoji({ question }: Props) {
const [value, setValue] = useState<string | null>(null);
const handleChange = (event: ChangeEvent<HTMLInputElement>) => {
setValue((event.target as HTMLInputElement).value);
};
return (
<FormControl fullWidth>
<FormLabel id="quiz-question-radio-group">{question.title}</FormLabel>
<RadioGroup
aria-labelledby="quiz-question-radio-group"
value={value}
onChange={handleChange}
>
{question.content.variants.map((variant, index) => (
<FormControlLabel key={index} value={variant.answer} control={<Radio />} label={
<Box sx={{ display: "flex", alignItems: "center", gap: 2 }}>
<Typography>{`${variant.emoji} ${variant.answer}`}</Typography>
<Tooltip title={variant.hints} placement="right">
<Box><InfoIcon /></Box>
</Tooltip>
</Box>
} />
))}
</RadioGroup>
</FormControl>
);
}

@ -0,0 +1,44 @@
import { Box, Button, Typography } from "@mui/material";
import { Question } from "@root/questions";
import { ChangeEvent, useRef, useState } from "react";
interface Props {
question: Question;
}
export default function File({ question }: Props) {
const fileInputRef = useRef<HTMLInputElement>(null);
const [files, setFiles] = useState<File[] | null>(null);
function handleFileChange(event: ChangeEvent<HTMLInputElement>) {
if (!event.target.files) return setFiles(null);
setFiles(Array.from(event.target.files));
}
return (
<Box sx={{
display: "flex",
flexDirection: "column",
gap: 1,
}}>
<Typography variant="h6">{question.title}</Typography>
<Box>
<Button
variant="contained"
onClick={() => fileInputRef.current?.click()}
>
Загрузить файл
<input
ref={fileInputRef}
onChange={handleFileChange}
type="file"
style={{
display: "none",
}}
/>
</Button>
</Box>
</Box>
);
}

@ -0,0 +1,41 @@
import InfoIcon from "@icons/InfoIcon";
import { Box, FormControl, FormControlLabel, FormLabel, Radio, RadioGroup, Tooltip, Typography } from "@mui/material";
import { Question } from "@root/questions";
import { ChangeEvent, useState } from "react";
interface Props {
question: Question;
}
export default function Images({ question }: Props) {
const [value, setValue] = useState<string | null>(null);
const handleChange = (event: ChangeEvent<HTMLInputElement>) => {
setValue((event.target as HTMLInputElement).value);
};
return (
<FormControl fullWidth>
<FormLabel id="quiz-question-radio-group">{question.title}</FormLabel>
<RadioGroup
aria-labelledby="quiz-question-radio-group"
value={value}
onChange={handleChange}
>
{question.content.variants.map((variant, index) => (
<FormControlLabel key={index} value={variant.answer} control={<Radio />} label={
<Box sx={{ display: "flex", alignItems: "center", gap: 2 }}>
<Typography>{variant.answer}</Typography>
<Tooltip title={variant.hints} placement="right">
<Box>
<InfoIcon />
</Box>
</Tooltip>
</Box>
} />
))}
</RadioGroup>
</FormControl>
);
}

@ -0,0 +1,35 @@
import { Box, Typography } from "@mui/material";
import { Question } from "@root/questions";
import { CustomSlider } from "@ui_kit/CustomSlider";
import { useState } from "react";
interface Props {
question: Question;
}
export default function Number({ question }: Props) {
const [sliderValue, setSliderValue] = useState<number>(question.content.start);
return (
<Box sx={{
display: "flex",
flexDirection: "column",
gap: 1,
}}>
<Typography variant="h6">{question.title}</Typography>
<Box sx={{
px: 2,
}}>
<CustomSlider
value={sliderValue}
onChange={v => setSliderValue(v as number)}
min={parseInt(question.content.range.split("—")[0])}
max={parseInt(question.content.range.split("—")[1])}
defaultValue={question.content.start}
step={question.content.step}
/>
</Box>
</Box>
);
}

@ -0,0 +1,23 @@
import { Box, Typography } from "@mui/material";
import { Question } from "@root/questions";
interface Props {
question: Question;
}
export default function Page({ question }: Props) {
return (
<Box sx={{
display: "flex",
flexDirection: "column",
gap: 1,
}}>
<Typography variant="h6">{question.title}</Typography>
<Box>
<Typography>{question.content.text}</Typography>
</Box>
</Box>
);
}

@ -0,0 +1,74 @@
import { Box, Typography, useMediaQuery, useTheme } from "@mui/material";
import { Question } from "@root/questions";
import { FC, useState } from "react";
import FlagIcon from "../../../assets/icons/questionsPage/FlagIcon";
import StarIconMini from "../../../assets/icons/questionsPage/StarIconMini";
import HashtagIcon from "../../../assets/icons/questionsPage/hashtagIcon";
import HeartIcon from "../../../assets/icons/questionsPage/heartIcon";
import LightbulbIcon from "../../../assets/icons/questionsPage/lightbulbIcon";
import LikeIcon from "../../../assets/icons/questionsPage/likeIcon";
import TropfyIcon from "../../../assets/icons/questionsPage/tropfyIcon";
type RatingIconType = "star" | "trophie" | "flag" | "heart" | "like" | "bubble" | "hashtag";
const ratingIconComponentByType: Record<RatingIconType, FC<{ color: string; }>> = {
"star": StarIconMini,
"trophie": TropfyIcon,
"flag": FlagIcon,
"heart": HeartIcon,
"like": LikeIcon,
"bubble": LightbulbIcon,
"hashtag": HashtagIcon,
};
interface Props {
question: Question;
}
export default function Rating({ question }: Props) {
const theme = useTheme();
const isMobile = useMediaQuery(theme.breakpoints.down(790));
const [selectedRating, setSelectedRating] = useState<number>(0);
const RatingIconComponent = ratingIconComponentByType[question.content.form as RatingIconType];
return (
<Box sx={{
display: "flex",
flexDirection: "column",
gap: 1,
}}>
<Typography variant="h6">{question.title}</Typography>
<Box sx={{
display: "flex",
gap: isMobile ? "10px" : "15px",
flexWrap: "wrap",
}}>
{Array.from(
{ length: question.content.steps },
(_, index) => index
).map((itemNumber) => (
<Box
key={itemNumber}
onClick={() => setSelectedRating(itemNumber + 1)}
sx={{
cursor: "pointer",
transform: "scale(1.5)",
":hover": {
transform: "scale(1.7)",
transition: "0.2s",
},
}}
>
<RatingIconComponent color={
selectedRating > itemNumber
? theme.palette.brightPurple.main
: theme.palette.grey2.main
} />
</Box>
))}
</Box>
</Box>
);
}

@ -0,0 +1,102 @@
import ArrowDownIcon from "@icons/ArrowDownIcon";
import { Box, FormControl, MenuItem, Select, SelectChangeEvent, Typography, useTheme } from "@mui/material";
import { Question } from "@root/questions";
import { useState } from "react";
interface Props {
question: Question;
}
export default function Text({ question }: Props) {
const theme = useTheme();
const [selectValue, setSelectValue] = useState<string>("");
function handleChange(event: SelectChangeEvent<string | null>) {
setSelectValue((event.target as HTMLInputElement).value);
}
return (
<Box sx={{
display: "flex",
flexDirection: "column",
gap: 1,
}}>
<Typography variant="h6">{question.title}</Typography>
<FormControl
fullWidth
size="small"
sx={{
width: "100%",
minWidth: "200px",
height: "48px",
}}
>
<Select
id="category-select"
variant="outlined"
value={selectValue}
displayEmpty
onChange={handleChange}
sx={{
height: "48px",
borderRadius: "8px",
"& .MuiOutlinedInput-notchedOutline": {
border: `1px solid ${theme.palette.brightPurple.main} !important`,
},
}}
MenuProps={{
PaperProps: {
sx: {
mt: "8px",
p: "4px",
borderRadius: "8px",
border: "1px solid #EEE4FC",
boxShadow: "0px 8px 24px rgba(210, 208, 225, 0.4)",
},
},
MenuListProps: {
sx: {
py: 0,
display: "flex",
flexDirection: "column",
gap: "8px",
"& .Mui-selected": {
backgroundColor: theme.palette.background.default,
color: theme.palette.brightPurple.main,
},
},
},
}}
inputProps={{
sx: {
color: theme.palette.brightPurple.main,
display: "flex",
alignItems: "center",
px: "9px",
gap: "20px",
},
}}
IconComponent={(props) => <ArrowDownIcon {...props} />}
>
{question.content.variants.map(variant => (
<MenuItem
key={variant.answer}
value={variant.answer}
sx={{
display: "flex",
alignItems: "center",
gap: "20px",
p: "4px",
borderRadius: "5px",
color: theme.palette.grey2.main,
}}
>
{variant.answer}
</MenuItem>
))}
</Select>
</FormControl>
</Box>
);
}

@ -0,0 +1,24 @@
import { Box, Typography } from "@mui/material";
import { Question } from "@root/questions";
import CustomTextField from "@ui_kit/CustomTextField";
interface Props {
question: Question;
}
export default function Text({ question }: Props) {
return (
<Box sx={{
display: "flex",
flexDirection: "column",
gap: 1,
}}>
<Typography variant="h6">{question.title}</Typography>
<CustomTextField
placeholder={question.content.placeholder}
/>
</Box>
);
}

@ -0,0 +1,41 @@
import InfoIcon from "@icons/InfoIcon";
import { Box, FormControl, FormControlLabel, FormLabel, Radio, RadioGroup, Tooltip, Typography } from "@mui/material";
import { Question } from "@root/questions";
import { ChangeEvent, useState } from "react";
interface Props {
question: Question;
}
export default function Variant({ question }: Props) {
const [value, setValue] = useState<string | null>(null);
const handleChange = (event: ChangeEvent<HTMLInputElement>) => {
setValue((event.target as HTMLInputElement).value);
};
return (
<FormControl fullWidth>
<FormLabel id="quiz-question-radio-group">{question.title}</FormLabel>
<RadioGroup
aria-labelledby="quiz-question-radio-group"
value={value}
onChange={handleChange}
>
{question.content.variants.map((variant, index) => (
<FormControlLabel key={index} value={variant.answer} control={<Radio />} label={
<Box sx={{ display: "flex", alignItems: "center", gap: 2 }}>
<Typography>{variant.answer}</Typography>
<Tooltip title={variant.hints} placement="right">
<Box>
<InfoIcon />
</Box>
</Tooltip>
</Box>
} />
))}
</RadioGroup>
</FormControl>
);
}

@ -0,0 +1,41 @@
import InfoIcon from "@icons/InfoIcon";
import { Box, FormControl, FormControlLabel, FormLabel, Radio, RadioGroup, Tooltip, Typography } from "@mui/material";
import { Question } from "@root/questions";
import { useState, ChangeEvent } from "react";
interface Props {
question: Question;
}
export default function Varimg({ question }: Props) {
const [value, setValue] = useState<string | null>(null);
const handleChange = (event: ChangeEvent<HTMLInputElement>) => {
setValue((event.target as HTMLInputElement).value);
};
return (
<FormControl fullWidth>
<FormLabel id="quiz-question-radio-group">{question.title}</FormLabel>
<RadioGroup
aria-labelledby="quiz-question-radio-group"
value={value}
onChange={handleChange}
>
{question.content.variants.map((variant, index) => (
<FormControlLabel key={index} value={variant.answer} control={<Radio />} label={
<Box sx={{ display: "flex", alignItems: "center", gap: 2 }}>
<Typography>{variant.answer}</Typography>
<Tooltip title={variant.hints} placement="right">
<Box>
<InfoIcon />
</Box>
</Tooltip>
</Box>
} />
))}
</RadioGroup>
</FormControl>
);
}

107
yarn.lock

@ -1048,6 +1048,13 @@
dependencies:
regenerator-runtime "^0.13.11"
"@babel/runtime@^7.23.1":
version "7.23.1"
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.23.1.tgz#72741dc4d413338a91dcb044a86f3c0bc402646d"
integrity sha512-hC2v6p8ZSI/W0HUzh3V8C5g+NwSKzKPtJwSpTjwl0o297GP9+ZLQSkdvHz46CM3LqyoXxq+5G9komY+eSqSO0g==
dependencies:
regenerator-runtime "^0.14.0"
"@babel/template@^7.18.10", "@babel/template@^7.20.7", "@babel/template@^7.3.3":
version "7.20.7"
resolved "https://registry.npmjs.org/@babel/template/-/template-7.20.7.tgz"
@ -1351,6 +1358,33 @@
minimatch "^3.1.2"
strip-json-comments "^3.1.1"
"@floating-ui/core@^1.4.2":
version "1.5.0"
resolved "https://registry.yarnpkg.com/@floating-ui/core/-/core-1.5.0.tgz#5c05c60d5ae2d05101c3021c1a2a350ddc027f8c"
integrity sha512-kK1h4m36DQ0UHGj5Ah4db7R0rHemTqqO0QLvUqi1/mUUp3LuAWbWxdxSIf/XsnH9VS6rRVPLJCncjRzUvyCLXg==
dependencies:
"@floating-ui/utils" "^0.1.3"
"@floating-ui/dom@^1.5.1":
version "1.5.3"
resolved "https://registry.yarnpkg.com/@floating-ui/dom/-/dom-1.5.3.tgz#54e50efcb432c06c23cd33de2b575102005436fa"
integrity sha512-ClAbQnEqJAKCJOEbbLo5IUlZHkNszqhuxS4fHAVxRPXPya6Ysf2G8KypnYcOTpx6I8xcgF9bbHb6g/2KpbV8qA==
dependencies:
"@floating-ui/core" "^1.4.2"
"@floating-ui/utils" "^0.1.3"
"@floating-ui/react-dom@^2.0.2":
version "2.0.2"
resolved "https://registry.yarnpkg.com/@floating-ui/react-dom/-/react-dom-2.0.2.tgz#fab244d64db08e6bed7be4b5fcce65315ef44d20"
integrity sha512-5qhlDvjaLmAst/rKb3VdlCinwTF4EYMiVxuuc/HVUjs46W0zgtbMmAZ1UTsDrRTxRmUEzl92mOtWbeeXL26lSQ==
dependencies:
"@floating-ui/dom" "^1.5.1"
"@floating-ui/utils@^0.1.3":
version "0.1.6"
resolved "https://registry.yarnpkg.com/@floating-ui/utils/-/utils-0.1.6.tgz#22958c042e10b67463997bd6ea7115fe28cbcaf9"
integrity sha512-OfX7E2oUDYxtBvsuS4e/jSn4Q9Qb6DzgeYtsAdkPZ47znpoNsMgZw0+tVijiv3uGNR6dgNlty6r9rzIzHjtd/A==
"@humanwhocodes/config-array@^0.11.6":
version "0.11.7"
resolved "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.7.tgz"
@ -1671,6 +1705,19 @@
prop-types "^15.8.1"
react-is "^18.2.0"
"@mui/base@^5.0.0-beta.17":
version "5.0.0-beta.18"
resolved "https://registry.yarnpkg.com/@mui/base/-/base-5.0.0-beta.18.tgz#f95d393cf80974e77c0823170cc15c854d5af84b"
integrity sha512-e9ZCy/ndhyt5MTshAS3qAUy/40UiO0jX+kAo6a+XirrPJE+rrQW+mKPSI0uyp+5z4Vh+z0pvNoJ2S2gSrNz3BQ==
dependencies:
"@babel/runtime" "^7.23.1"
"@floating-ui/react-dom" "^2.0.2"
"@mui/types" "^7.2.5"
"@mui/utils" "^5.14.12"
"@popperjs/core" "^2.11.8"
clsx "^2.0.0"
prop-types "^15.8.1"
"@mui/core-downloads-tracker@^5.10.16":
version "5.10.16"
resolved "https://registry.npmjs.org/@mui/core-downloads-tracker/-/core-downloads-tracker-5.10.16.tgz"
@ -1739,6 +1786,11 @@
resolved "https://registry.npmjs.org/@mui/types/-/types-7.2.2.tgz"
integrity sha512-siex8cZDtWeC916cXOoUOnEQQejuMYmHtc4hM6VkKVYaBICz3VIiqyiAomRboTQHt2jchxQ5Q5ATlbcDekTxDA==
"@mui/types@^7.2.5":
version "7.2.5"
resolved "https://registry.yarnpkg.com/@mui/types/-/types-7.2.5.tgz#cd62a1fc5eb1044137ccab2053b431dd7cfc3cb8"
integrity sha512-S2BwfNczr7VwS6ki8GoAXJyARoeSJDLuxOEPs3vEMyTALlf9PrdHv+sluX7kk3iKrCg/ML2mIWwapZvWbkMCQA==
"@mui/utils@^5.10.16":
version "5.10.16"
resolved "https://registry.npmjs.org/@mui/utils/-/utils-5.10.16.tgz"
@ -1750,6 +1802,29 @@
prop-types "^15.8.1"
react-is "^18.2.0"
"@mui/utils@^5.14.11", "@mui/utils@^5.14.12":
version "5.14.12"
resolved "https://registry.yarnpkg.com/@mui/utils/-/utils-5.14.12.tgz#58b570839e22e0fba71e17d37d9c083fe233704d"
integrity sha512-RFNXnhKQlzIkIUig6mmv0r5VbtjPdWoaBPYicq25LETdZux59HAqoRdWw15T7lp3c7gXOoE8y67+hTB8C64m2g==
dependencies:
"@babel/runtime" "^7.23.1"
"@types/prop-types" "^15.7.7"
prop-types "^15.8.1"
react-is "^18.2.0"
"@mui/x-date-pickers@^6.16.1":
version "6.16.1"
resolved "https://registry.yarnpkg.com/@mui/x-date-pickers/-/x-date-pickers-6.16.1.tgz#76341384ef51db5f405f779fe5f2e9456b2cdc53"
integrity sha512-4B2+DU7aywYdvmr10o2qai6kbbR26zta/v1y8x3bmTilI/KcbhZ2OlsyArPKmTRNC8VYirejSnhLkPR/+JIkPg==
dependencies:
"@babel/runtime" "^7.23.1"
"@mui/base" "^5.0.0-beta.17"
"@mui/utils" "^5.14.11"
"@types/react-transition-group" "^4.4.7"
clsx "^2.0.0"
prop-types "^15.8.1"
react-transition-group "^4.4.5"
"@nicolo-ribaudo/eslint-scope-5-internals@5.1.1-v1":
version "5.1.1-v1"
resolved "https://registry.npmjs.org/@nicolo-ribaudo/eslint-scope-5-internals/-/eslint-scope-5-internals-5.1.1-v1.tgz"
@ -1798,6 +1873,11 @@
resolved "https://registry.npmjs.org/@popperjs/core/-/core-2.11.6.tgz"
integrity sha512-50/17A98tWUfQ176raKiOGXuYpLyyVMkxxG6oylzL3BPOlA6ADGdK7EYunSa4I064xerltq9TGXs8HmOk5E+vw==
"@popperjs/core@^2.11.8":
version "2.11.8"
resolved "https://registry.yarnpkg.com/@popperjs/core/-/core-2.11.8.tgz#6b79032e760a0899cd4204710beede972a3a185f"
integrity sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==
"@react-dnd/asap@^5.0.1":
version "5.0.2"
resolved "https://registry.yarnpkg.com/@react-dnd/asap/-/asap-5.0.2.tgz#1f81f124c1cd6f39511c11a881cfb0f715343488"
@ -2279,6 +2359,11 @@
resolved "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.5.tgz"
integrity sha512-JCB8C6SnDoQf0cNycqd/35A7MjcnK+ZTqE7judS6o7utxUCg6imJg3QK2qzHKszlTjcj2cn+NwMB2i96ubpj7w==
"@types/prop-types@^15.7.7":
version "15.7.8"
resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.7.8.tgz#805eae6e8f41bd19e88917d2ea200dc992f405d3"
integrity sha512-kMpQpfZKSCBqltAJwskgePRaYRFukDkm1oItcAbC3gNELR20XIBcN9VRgg4+m8DKsTfkWeA4m4Imp4DDuWy7FQ==
"@types/q@^1.5.1":
version "1.5.5"
resolved "https://registry.npmjs.org/@types/q/-/q-1.5.5.tgz"
@ -2339,6 +2424,13 @@
dependencies:
"@types/react" "*"
"@types/react-transition-group@^4.4.7":
version "4.4.7"
resolved "https://registry.yarnpkg.com/@types/react-transition-group/-/react-transition-group-4.4.7.tgz#bf69f269d74aa78b99097673ca6dd6824a68ef1c"
integrity sha512-ICCyBl5mvyqYp8Qeq9B5G/fyBSRC0zx3XM3sCC6KkcMsNeAHqXBKkmat4GqdJET5jtYUpZXrxI5flve5qhi2Eg==
dependencies:
"@types/react" "*"
"@types/react@*", "@types/react@^18.0.0":
version "18.0.26"
resolved "https://registry.npmjs.org/@types/react/-/react-18.0.26.tgz"
@ -3405,6 +3497,11 @@ clsx@^1.1.0, clsx@^1.1.1, clsx@^1.2.1:
resolved "https://registry.npmjs.org/clsx/-/clsx-1.2.1.tgz"
integrity sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg==
clsx@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/clsx/-/clsx-2.0.0.tgz#12658f3fd98fafe62075595a5c30e43d18f3d00b"
integrity sha512-rQ1+kcj+ttHG0MKVGBUXwayCCF1oh39BF5COIpRzuCEv8Mwjv0XucrI2ExNTOn9IlLifGClWQcU9BrZORvtw6Q==
co@^4.6.0:
version "4.6.0"
resolved "https://registry.npmjs.org/co/-/co-4.6.0.tgz"
@ -3849,6 +3946,11 @@ data-urls@^2.0.0:
whatwg-mimetype "^2.3.0"
whatwg-url "^8.0.0"
dayjs@^1.11.10:
version "1.11.10"
resolved "https://registry.yarnpkg.com/dayjs/-/dayjs-1.11.10.tgz#68acea85317a6e164457d6d6947564029a6a16a0"
integrity sha512-vjAczensTgRcqDERK0SR2XMwsF/tSvnvlv6VcF2GIhg6Sx4yOIt/irsr1RDJsKiIyBzJDpCoXiWWq28MqH2cnQ==
debug@2.6.9, debug@^2.6.0, debug@^2.6.9:
version "2.6.9"
resolved "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz"
@ -8117,6 +8219,11 @@ regenerator-runtime@^0.13.11, regenerator-runtime@^0.13.9:
resolved "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz"
integrity sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==
regenerator-runtime@^0.14.0:
version "0.14.0"
resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.14.0.tgz#5e19d68eb12d486f797e15a3c6a918f7cec5eb45"
integrity sha512-srw17NI0TUWHuGa5CFGGmhfNIeja30WMBfbslPNhf6JrqQlLN5gcrvig1oqPxiVaXb0oW0XRKtH6Nngs5lKCIA==
regenerator-transform@^0.15.1:
version "0.15.1"
resolved "https://registry.npmjs.org/regenerator-transform/-/regenerator-transform-0.15.1.tgz"