Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Strongly type all Quiz data types [refactor] #11992

Merged
merged 5 commits into from
Feb 1, 2024
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/components/Quiz/QuizWidget/AnswerIcon.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { CorrectIcon, IncorrectIcon, TrophyIcon } from "../../icons/quiz"

import { AnswerStatus } from "./useQuizWidget"

interface AnswerIconProps {
type AnswerIconProps = {
answerStatus: AnswerStatus
}

Expand Down
4 changes: 2 additions & 2 deletions src/components/Quiz/QuizWidget/QuizRadioGroup.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import {
VisuallyHidden,
} from "@chakra-ui/react"

import { TranslationKey } from "@/lib/types"
import type { AnswerKey, TranslationKey } from "@/lib/types"

import { useQuizWidgetContext } from "./context"

Expand All @@ -28,7 +28,7 @@ export const QuizRadioGroup = () => {
} = useQuizWidgetContext()
const { t } = useTranslation("learn-quizzes")

const handleSelection = (answerId: string) => {
const handleSelection = (answerId: AnswerKey) => {
const isCorrect =
answerId === questions[currentQuestionIndex].correctAnswerId
setCurrentQuestionAnswerChoice({ answerId, isCorrect })
Expand Down
15 changes: 7 additions & 8 deletions src/components/Quiz/QuizWidget/context.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import { createContext, Dispatch, SetStateAction, useContext } from "react"

import { QuizStatus, UserStats } from "@/lib/types"
import { AnswerChoice, Quiz } from "@/lib/interfaces"
import { AnswerChoice, Quiz, QuizKey, QuizStatus, UserStats } from "@/lib/types"

import { AnswerStatus } from "./useQuizWidget"

Expand All @@ -12,12 +11,12 @@ type QuizWidgetContextType = Quiz & {
showResults: boolean
currentQuestionAnswerChoice: AnswerChoice | null
quizPageProps:
| {
currentHandler: (nextKey: string) => void
statusHandler: (status: QuizStatus) => void
nextQuiz: string | undefined
}
| false
| {
currentHandler: (nextKey: QuizKey) => void
statusHandler: (status: QuizStatus) => void
nextQuiz: QuizKey | undefined
}
| false
numberOfCorrectAnswers: number
quizScore: number
ratioCorrect: number
Expand Down
8 changes: 4 additions & 4 deletions src/components/Quiz/QuizWidget/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import {
VStack,
} from "@chakra-ui/react"

import { QuizStatus, UserStats } from "@/lib/types"
import type { QuizKey, QuizStatus, UserStats } from "@/lib/types"

import Translation from "@/components/Translation"

Expand All @@ -25,7 +25,7 @@ import { QuizSummary } from "./QuizSummary"
import { useQuizWidget } from "./useQuizWidget"

type CommonProps = {
quizKey: string
quizKey: QuizKey
updateUserStats: Dispatch<SetStateAction<UserStats>>
}

Expand All @@ -36,7 +36,7 @@ type StandaloneQuizProps = CommonProps & {
}

type QuizPageProps = CommonProps & {
currentHandler: (nextKey: string) => void
currentHandler: (nextKey: QuizKey) => void
statusHandler: (status: QuizStatus) => void
isStandaloneQuiz?: false
}
Expand Down Expand Up @@ -71,7 +71,7 @@ const QuizWidget = ({

const quizPageProps = useRef<
| (Required<Pick<QuizWidgetProps, "currentHandler" | "statusHandler">> & {
nextQuiz: string | undefined
nextQuiz: QuizKey | undefined
})
| false
>(false)
Expand Down
33 changes: 10 additions & 23 deletions src/components/Quiz/QuizWidget/useQuizWidget.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
import { useEffect, useMemo, useRef, useState } from "react"
import { useEffect, useMemo, useState } from "react"
import { shuffle } from "lodash"
import { useTranslation } from "next-i18next"

import {
import type {
AnswerChoice,
Question,
Quiz,
QuizKey,
RawQuestion,
RawQuiz,
} from "@/lib/interfaces"
} from "@/lib/types"

import allQuizzesData from "@/data/quizzes"
import questionBank from "@/data/quizzes/questionBank"
Expand All @@ -28,11 +29,9 @@ export const useQuizWidget = ({
const { t } = useTranslation()

const [quizData, setQuizData] = useState<Quiz | null>(null)
const [nextQuiz, setNextQuiz] = useState<string | undefined>(undefined)
const [userQuizProgress, setUserQuizProgress] = useState<Array<AnswerChoice>>(
[]
)
const [showAnswer, setShowAnswer] = useState<boolean>(false)
const [nextQuiz, setNextQuiz] = useState<QuizKey | undefined>(undefined)
const [userQuizProgress, setUserQuizProgress] = useState<AnswerChoice[]>([])
const [showAnswer, setShowAnswer] = useState(false)
const [currentQuestionAnswerChoice, setCurrentQuestionAnswerChoice] =
useState<AnswerChoice | null>(null)

Expand All @@ -46,18 +45,9 @@ export const useQuizWidget = ({
setUserQuizProgress([])
setShowAnswer(false)

const currentQuizKey =
quizKey ||
Object.keys(allQuizzesData).filter((quizUri) =>
window?.location.href.includes(quizUri)
)[0] ||
null
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I did a search and it seems that we always pass a quizKey, this default is never used. I'm ok with removing it if not used, but just curious about its original purpose, do you know it?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm honestly not sure... but that was my same reasoning... there was no circumstance where the fallback would be used since a valid key is required.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thinking a little here, it may have simply been a product of not having proper typing, so any string could be used which may-or-may-not be available. This shouldn't be an issue now.


if (!currentQuizKey) return

// Get quiz data from key, shuffle, then truncate if necessary
const rawQuiz: RawQuiz = allQuizzesData[currentQuizKey]
const questions: Array<Question> = rawQuiz.questions.map((id) => {
const rawQuiz: RawQuiz = allQuizzesData[quizKey]
const questions: Question[] = rawQuiz.questions.map((id) => {
const rawQuestion: RawQuestion = questionBank[id]
return { id, ...rawQuestion }
})
Expand Down Expand Up @@ -99,10 +89,7 @@ export const useQuizWidget = ({
const quizScore = Math.floor(ratioCorrect * 100)
const isPassingScore = quizScore > PASSING_QUIZ_SCORE

const showConfetti = useMemo<boolean>(
() => !!quizData && showResults && isPassingScore,
[quizData, showResults, isPassingScore]
)
const showConfetti = !!quizData && showResults && isPassingScore
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Makes sense 👍🏼


useEffect(() => {
if (!showResults) return
Expand Down
8 changes: 4 additions & 4 deletions src/components/Quiz/QuizzesList.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import React from "react"
import { Heading, OrderedList, Stack, Text } from "@chakra-ui/react"

import type { QuizzesSection, UserStats } from "@/lib/types"
import type { QuizKey, QuizzesSection, UserStats } from "@/lib/types"

import { trackCustomEvent } from "@/lib/utils/matomo"

Expand All @@ -11,12 +11,12 @@ import Translation from "../Translation"

import QuizItem from "./QuizItem"

export interface QuizzesListProps {
type QuizzesListProps = {
userStats: UserStats
content: Array<QuizzesSection>
content: QuizzesSection[]
headingId: string
descriptionId: string
quizHandler: (id: string) => void
quizHandler: (id: QuizKey) => void
modalHandler: (isModalOpen: boolean) => void
}

Expand Down
10 changes: 7 additions & 3 deletions src/components/Quiz/QuizzesModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,16 @@ import {

import { QuizStatus } from "@/lib/types"

interface IProps extends Omit<ModalProps, "isCentered" | "scrollBehavior"> {
type QuizzesModalProps = Omit<ModalProps, "isCentered" | "scrollBehavior"> & {
children: React.ReactNode
quizStatus: QuizStatus
}

const QuizzesModal: React.FC<IProps> = ({ children, quizStatus, ...rest }) => {
const QuizzesModal = ({
children,
quizStatus,
...props
}: QuizzesModalProps) => {
const getStatusColor = (): ModalContentProps["bg"] => {
if (quizStatus === "neutral") {
return "neutral"
Expand All @@ -31,7 +35,7 @@ const QuizzesModal: React.FC<IProps> = ({ children, quizStatus, ...rest }) => {
isCentered
size={{ base: "full", md: "xl" }}
scrollBehavior="inside"
{...rest}
{...props}
>
<ModalOverlay bg="blackAlpha.700" />

Expand Down
19 changes: 2 additions & 17 deletions src/components/Quiz/useLocalQuizData.ts
Original file line number Diff line number Diff line change
@@ -1,28 +1,13 @@
import { CompletedQuizzes, UserStats } from "@/lib/types"

import allQuizzesData from "@/data/quizzes"

import { USER_STATS_KEY } from "@/lib/constants"

import { useLocalStorage } from "@/hooks/useLocalStorage"

/**
* Contains each quiz id as key and <boolean, number> to indicate if its completed and the highest score in that quiz
*
* Initialize all quizzes as not completed
*/
const INITIAL_COMPLETED_QUIZZES: CompletedQuizzes = Object.keys(
allQuizzesData
).reduce((object, key) => ({ ...object, [key]: [false, 0] }), {})

export const INITIAL_USER_STATS: UserStats = {
score: 0,
average: [],
completed: INITIAL_COMPLETED_QUIZZES,
completed: {} as CompletedQuizzes,
}

export const useLocalQuizData = () => {
const data = useLocalStorage(USER_STATS_KEY, INITIAL_USER_STATS)

return data
}
export const useLocalQuizData = () => useLocalStorage<UserStats>(USER_STATS_KEY, INITIAL_USER_STATS)
8 changes: 3 additions & 5 deletions src/data/quizzes/index.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
// Import data types
import type { QuizzesSection } from "@/lib/types"
import type { RawQuizzes } from "@/lib/interfaces"
import type { QuizzesSection, RawQuizzes } from "@/lib/types"

// Declare hash-map of quizzes based on slug key
const quizzes: RawQuizzes = {
const quizzes = {
"what-is-ethereum": {
title: "what-is-ethereum",
questions: ["a001", "a002", "a003", "a004", "a005"],
Expand Down Expand Up @@ -36,7 +34,7 @@ const quizzes: RawQuizzes = {
title: "learn-quizzes:page-assets-merge",
questions: ["h001", "h002", "h003", "h004", "h005"],
},
}
} satisfies RawQuizzes

export const ethereumBasicsQuizzes: QuizzesSection[] = [
{
Expand Down
6 changes: 3 additions & 3 deletions src/data/quizzes/questionBank.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
// Import data types
import type { QuestionBank } from "@/lib/interfaces"
import type { QuestionBank } from "@/lib/types"

// Declare hash map of question bank
const questionBank: QuestionBank = {
const questionBank = {
// What is Ethereum?
a001: {
prompt: "a001-prompt",
Expand Down Expand Up @@ -887,6 +887,6 @@ const questionBank: QuestionBank = {
],
correctAnswerId: "h005-b",
},
}
} as const satisfies QuestionBank
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a benefit doing as const satisfies instead of just satisfies?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I tried without, but had to add back as const (in this commit) after it wasn't casting the typeof as literals to perform keyof on... instead it was just simplified to string (IIRC)...

The other place satisfies is used didn't give me that problem, so I kept as const off of that one.


export default questionBank
45 changes: 0 additions & 45 deletions src/lib/interfaces.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { StaticImageData } from "next/image"
import type { ReactNode } from "react"

import type {
CrowdinContributor,
Expand All @@ -10,50 +9,6 @@ import type {
TranslationKey,
} from "@/lib/types"

/**
* Quiz data interfaces
*/
export interface AnswerChoice {
answerId: string
isCorrect: boolean
}

export interface Answer {
id: string
label: TranslationKey
explanation: TranslationKey
moreInfoLabel?: string
moreInfoUrl?: string
}

export interface RawQuestion {
prompt: TranslationKey
answers: Answer[]
correctAnswerId: string
}

export interface Question extends RawQuestion {
id: string
}

export interface QuestionBank {
[key: string]: RawQuestion
}

export interface RawQuiz {
title: TranslationKey
questions: string[] // TODO: Force to be an array of questionID's
}

export interface Quiz {
title: string
questions: Question[]
}

export interface RawQuizzes {
[key: string]: RawQuiz
}

export interface DeveloperDocsLink {
id: TranslationKey
to: string
Expand Down
Loading
Loading