Skip to content

Commit 445ddf0

Browse files
authored
Merge pull request #11423 from TylerAPfledderer/refactor/quiz-widget-ui
refactor(Quiz): update and reorganize UI
2 parents 7854a8c + b4a7041 commit 445ddf0

20 files changed

+950
-749
lines changed

src/components/Quiz/QuizItem.tsx

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,6 @@ const QuizItem: React.FC<QuizzesListItem> = (props) => {
5656
<Flex gap={2} alignItems="center">
5757
<Text
5858
color={isCompleted ? "body.medium" : "text"}
59-
fontWeight="bold"
6059
_before={{
6160
content: 'counter(list-counter) ". "',
6261
}}
@@ -65,9 +64,7 @@ const QuizItem: React.FC<QuizzesListItem> = (props) => {
6564
</Text>
6665

6766
{/* Show green tick if quizz was completed only */}
68-
<Box display={isCompleted ? "flex" : "none"}>
69-
<GreenTickIcon />
70-
</Box>
67+
{isCompleted && <GreenTickIcon />}
7168
</Flex>
7269

7370
{/* Labels */}
@@ -86,7 +83,7 @@ const QuizItem: React.FC<QuizzesListItem> = (props) => {
8683
{/* Start Button */}
8784
<Box w={{ base: "full", lg: "auto" }}>
8885
<Button
89-
variant="outline-color"
86+
variant="outline"
9087
w={{ base: "full", lg: "auto" }}
9188
onClick={handleStart}
9289
>

src/components/Quiz/QuizRadioGroup.tsx

Lines changed: 96 additions & 79 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,12 @@ import {
44
chakra,
55
Circle,
66
Flex,
7+
FlexProps,
78
RadioProps,
9+
SquareProps,
10+
Stack,
811
Text,
12+
TextProps,
913
useRadio,
1014
useRadioGroup,
1115
useToken,
@@ -21,6 +25,90 @@ import { Question } from "../../types"
2125
interface CustomRadioProps extends RadioProps {
2226
index: number
2327
label: string
28+
showAnswer: boolean
29+
isSelectedCorrect: boolean
30+
}
31+
32+
const CustomRadio: React.FC<CustomRadioProps> = ({
33+
index,
34+
label,
35+
showAnswer,
36+
isSelectedCorrect,
37+
...radioProps
38+
}) => {
39+
const { state, getInputProps, getRadioProps, htmlProps } =
40+
useRadio(radioProps)
41+
42+
// Memoized values
43+
const buttonBg = useMemo<string>(() => {
44+
if (!state.isChecked) return "body.inverted"
45+
if (!showAnswer) return "primary.base"
46+
if (!isSelectedCorrect) return "error.base"
47+
return "success.base"
48+
}, [state.isChecked, showAnswer, isSelectedCorrect])
49+
50+
const primaryBaseColor = useToken("colors", "primary.base")
51+
52+
const focusProps: FlexProps = {
53+
outline: showAnswer ? "none" : `1px solid ${primaryBaseColor}`,
54+
}
55+
56+
const controlFocusProps: SquareProps = {
57+
bg: showAnswer ? "white" : "primary.pressed",
58+
}
59+
60+
const getRadioControlBg = (): SquareProps["bg"] => {
61+
if (showAnswer) return "white"
62+
63+
if (state.isChecked) return "primary.pressed"
64+
65+
return "disabled"
66+
}
67+
68+
const getControlLabelColor = (): TextProps["color"] => {
69+
if (!showAnswer) return "white"
70+
71+
if (isSelectedCorrect) return "success.base"
72+
73+
return "error.base"
74+
}
75+
76+
// Render CustomRadio component
77+
return (
78+
<chakra.label {...htmlProps} cursor="pointer" w="100%">
79+
<input {...getInputProps()} />
80+
<Flex
81+
{...getRadioProps()}
82+
w="100%"
83+
p={2}
84+
alignItems="center"
85+
bg={buttonBg}
86+
color={state.isChecked ? "white" : "text"}
87+
borderRadius="base"
88+
_hover={{ ...focusProps, cursor: showAnswer ? "default" : "pointer" }}
89+
_focus={focusProps}
90+
data-group
91+
>
92+
<Circle
93+
size="25px"
94+
bg={getRadioControlBg()}
95+
_groupHover={controlFocusProps}
96+
_groupFocus={controlFocusProps}
97+
me={2}
98+
>
99+
<Text
100+
m="0"
101+
fontWeight="700"
102+
fontSize="lg"
103+
color={getControlLabelColor()}
104+
>
105+
{String.fromCharCode(97 + index).toUpperCase()}
106+
</Text>
107+
</Circle>
108+
{label}
109+
</Flex>
110+
</chakra.label>
111+
)
24112
}
25113

26114
interface IProps {
@@ -52,91 +140,18 @@ const QuizRadioGroup: React.FC<IProps> = ({
52140
[selectedAnswer]
53141
)
54142

55-
// Custom radio button component
56-
const CustomRadio: React.FC<CustomRadioProps> = ({
57-
index,
58-
label,
59-
...radioProps
60-
}) => {
61-
const { state, getInputProps, getCheckboxProps, htmlProps } =
62-
useRadio(radioProps)
63-
64-
// Memoized values
65-
const buttonBg = useMemo<string>(() => {
66-
if (!state.isChecked) return "body.inverted"
67-
if (!showAnswer) return "primary.base"
68-
if (!isSelectedCorrect) return "error.base"
69-
return "success.base"
70-
}, [state.isChecked, showAnswer, isSelectedCorrect])
71-
72-
const primaryBaseColor = useToken("colors", "primary.base")
73-
74-
// Render CustomRadio component
75-
return (
76-
<chakra.label {...htmlProps} cursor="pointer" data-group w="100%">
77-
<input {...getInputProps({})} hidden />
78-
<Flex
79-
{...getCheckboxProps()}
80-
w="100%"
81-
p={2}
82-
alignItems="center"
83-
bg={buttonBg}
84-
color={state.isChecked ? "white" : "text"}
85-
borderRadius="base"
86-
_hover={{
87-
boxShadow: showAnswer ? "none" : "primary.base",
88-
outline: showAnswer ? "none" : `1px solid ${primaryBaseColor}`,
89-
cursor: showAnswer ? "default" : "pointer",
90-
}}
91-
>
92-
<Circle
93-
size="25px"
94-
bg={
95-
showAnswer
96-
? "white"
97-
: state.isChecked
98-
? "primary.pressed"
99-
: "disabled"
100-
}
101-
_groupHover={{
102-
bg: showAnswer ? "white" : "primary.pressed",
103-
}}
104-
me={2}
105-
>
106-
<Text
107-
m="0"
108-
fontWeight="700"
109-
fontSize="lg"
110-
color={
111-
!showAnswer
112-
? "white"
113-
: isSelectedCorrect
114-
? "success.base"
115-
: "error.base"
116-
}
117-
>
118-
{String.fromCharCode(97 + index).toUpperCase()}
119-
</Text>
120-
</Circle>
121-
{label}
122-
</Flex>
123-
</chakra.label>
124-
)
125-
}
126-
127143
// Render QuizRadioGroup
128144
return (
129-
<Flex {...getRootProps()} direction="column" w="100%">
145+
<Stack spacing="6" {...getRootProps()} w="100%">
130146
<Text
131147
textAlign={{ base: "center", md: "left" }}
132148
fontWeight="700"
133-
fontSize="2xl"
134-
mb={6}
149+
size="2xl"
135150
>
136151
{t(prompt)}
137152
</Text>
138153

139-
<Flex direction="column" gap={4}>
154+
<Stack gap="4">
140155
{answers.map(({ id, label }, index) => {
141156
const display =
142157
!showAnswer || id === selectedAnswer ? "inline-flex" : "none"
@@ -146,21 +161,23 @@ const QuizRadioGroup: React.FC<IProps> = ({
146161
display={display}
147162
index={index}
148163
label={t(label)}
164+
showAnswer={showAnswer}
165+
isSelectedCorrect={isSelectedCorrect}
149166
{...getRadioProps({ value: id })}
150167
/>
151168
)
152169
})}
153-
</Flex>
170+
</Stack>
154171

155172
{showAnswer && (
156-
<Box mt={5}>
173+
<Box>
157174
<Text fontWeight="bold" mt={0} mb={2}>
158175
<Translation id="explanation" />
159176
</Text>
160177
<Text m={0}>{t(explanation)}</Text>
161178
</Box>
162179
)}
163-
</Flex>
180+
</Stack>
164181
)
165182
}
166183

src/components/Quiz/QuizSummary.tsx

Lines changed: 24 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,14 @@
11
import React, { useEffect } from "react"
2-
import { Box, Flex, useMediaQuery } from "@chakra-ui/react"
2+
import {
3+
HStack,
4+
StackDivider,
5+
Text,
6+
useMediaQuery,
7+
VStack,
8+
} from "@chakra-ui/react"
39
import { useI18next } from "gatsby-plugin-react-i18next"
410

511
import Translation from "../Translation"
6-
import Text from "../OldText"
712

813
import { numberToPercent } from "../../utils/numberToPercent"
914
import { updateUserStats } from "./utils"
@@ -31,7 +36,7 @@ const QuizSummary: React.FC<IProps> = ({
3136
const { language } = useI18next()
3237
const [largerThanMobile] = useMediaQuery("(min-width: 30em)")
3338

34-
const valueStyles = { fontWeight: "700", mb: 2 }
39+
const valueStyles = { fontWeight: "700", lineHeight: 1 }
3540
const labelStyles = { fontSize: "sm", m: 0, color: "disabled" }
3641

3742
// QuizSummary is rendered when user has finished the quiz, proper time to update the stats
@@ -45,7 +50,7 @@ const QuizSummary: React.FC<IProps> = ({
4550
}, [])
4651

4752
return (
48-
<Box w="full" fontSize={["xl", "2xl"]}>
53+
<VStack spacing="3" w="full" fontSize={["xl", "2xl"]}>
4954
<Text
5055
fontWeight="700"
5156
textAlign="center"
@@ -59,52 +64,48 @@ const QuizSummary: React.FC<IProps> = ({
5964
)}
6065
</Text>
6166

62-
<Flex
63-
p={4}
67+
<HStack
68+
py="4"
69+
px="8"
6470
justify="center"
6571
boxShadow="drop"
6672
bg="background.base"
6773
mx="auto"
68-
w="fit-content"
74+
spacing="4"
6975
sx={{
70-
"div:not(:last-of-type)": {
71-
borderEnd: "1px",
72-
borderColor: "disabled",
73-
},
74-
div: {
75-
p: 4,
76-
flexDirection: "column",
77-
alignItems: "center",
76+
"& > div": {
77+
py: "4",
7878
},
7979
}}
8080
overflowX="hidden"
81+
divider={<StackDivider borderColor="disabled" />}
8182
>
82-
<Flex>
83+
<VStack>
8384
<Text {...valueStyles}>
8485
{numberToPercent(ratioCorrect, language)}
8586
</Text>
8687
<Text {...labelStyles}>
8788
<Translation id="score" />
8889
</Text>
89-
</Flex>
90+
</VStack>
9091

91-
<Flex>
92+
<VStack>
9293
<Text {...valueStyles}>+{numberOfCorrectAnswers}</Text>
9394
<Text {...labelStyles}>
9495
<Translation id="correct" />
9596
</Text>
96-
</Flex>
97+
</VStack>
9798

9899
{largerThanMobile && (
99-
<Flex>
100+
<VStack>
100101
<Text {...valueStyles}>{questionCount}</Text>
101102
<Text {...labelStyles}>
102103
<Translation id="questions" />
103104
</Text>
104-
</Flex>
105+
</VStack>
105106
)}
106-
</Flex>
107-
</Box>
107+
</HStack>
108+
</VStack>
108109
)
109110
}
110111

0 commit comments

Comments
 (0)