diff --git a/projects/app/src/client/ui/QuizDeck.tsx b/projects/app/src/client/ui/QuizDeck.tsx index b9cae0f70d..ff14c61d1a 100644 --- a/projects/app/src/client/ui/QuizDeck.tsx +++ b/projects/app/src/client/ui/QuizDeck.tsx @@ -1,3 +1,7 @@ +import { + autoCheckUserSetting, + useUserSetting, +} from "@/client/hooks/useUserSetting"; import { useEventCallback } from "@/client/hooks/useEventCallback"; import { usePrefetchImages } from "@/client/hooks/usePrefetchImages"; import { useQuizProgress } from "@/client/hooks/useQuizProgress"; @@ -30,9 +34,12 @@ import React, { useEffect, useId, useRef, useState } from "react"; import { Animated as RnAnimated, Text, View } from "react-native"; import Reanimated, { FadeIn } from "react-native-reanimated"; import { CloseButton } from "./CloseButton"; +import { Delay } from "./Delay"; +import { IconImage } from "./IconImage"; import { usePostHog } from "./PostHogProvider"; import { QuizDeckHanziToPinyinQuestion } from "./QuizDeckHanziToPinyinQuestion"; import { QuizDeckOneCorrectPairQuestion } from "./QuizDeckOneCorrectPairQuestion"; +import { QuizDeckToastContainer } from "./QuizDeckToastContainer"; import { QuizProgressBar } from "./QuizProgressBar"; import { QuizQueueButton } from "./QuizQueueButton"; import { RectButton } from "./RectButton"; @@ -55,6 +62,9 @@ export const QuizDeck = ({ className }: { className?: string }) => { const queryClient = useQueryClient(); const postHog = usePostHog(); + const autoCheck = + useUserSetting(autoCheckUserSetting).value?.enabled ?? false; + const query = nextQuizQuestionQuery(r, id); // The following is a bit convoluted but allows prefetching the next question @@ -67,6 +77,10 @@ export const QuizDeck = ({ className }: { className?: string }) => { const reviewQueue = nextQuestionQuery.data?.reviewQueue ?? null; const [question, setQuestion] = useState(); + const [toastState, setToastState] = useState<{ + correct: boolean; + show: boolean; + } | null>(null); useEffect(() => { if (question == null && nextQuestion != null) { @@ -90,6 +104,8 @@ export const QuizDeck = ({ className }: { className?: string }) => { const handleNext = useEventCallback(() => { // Clear the current question so that we swap to the next question. setQuestion(undefined); + // Clear the toast + setToastState(null); }); const handleRating = useEventCallback( @@ -107,6 +123,15 @@ export const QuizDeck = ({ className }: { className?: string }) => { playSuccessSound(); } + // Show the toast + setToastState({ correct: success, show: true }); + + // If auto-check is enabled and the answer is correct, advance immediately + if (autoCheck && success) { + // Clear the current question immediately to start transition + setQuestion(undefined); + } + const now = Date.now(); void (async () => { @@ -289,6 +314,47 @@ export const QuizDeck = ({ className }: { className?: string }) => { + + {/* Toast shown at deck level */} + {toastState?.show === true ? ( + + + {toastState.correct ? ( + <> + + + Nice! + + { + setToastState(null); + }} + /> + + ) : ( + + + Incorrect + + )} + + + ) : null} ); }; diff --git a/projects/app/src/client/ui/QuizDeckHanziToPinyinQuestion.tsx b/projects/app/src/client/ui/QuizDeckHanziToPinyinQuestion.tsx index a7df4a17cb..e594dd219f 100644 --- a/projects/app/src/client/ui/QuizDeckHanziToPinyinQuestion.tsx +++ b/projects/app/src/client/ui/QuizDeckHanziToPinyinQuestion.tsx @@ -9,7 +9,6 @@ import type { PinyinPronunciation, UnsavedSkillRating, } from "@/data/model"; -import { SkillKind } from "@/data/model"; import type { PinyinSyllableSuggestion, PinyinSyllableSuggestions, @@ -19,11 +18,9 @@ import { pinyinSyllableSuggestions, } from "@/data/pinyin"; import { hanziToPinyinQuestionMistakes } from "@/data/questions/hanziWordToPinyin"; -import type { HanziWordSkill, Skill } from "@/data/rizzleSchema"; import { computeSkillRating, hanziWordFromSkill, - skillKindFromSkill, } from "@/data/skills"; import { hanziFromHanziWord } from "@/dictionary/dictionary"; import { nonNullable } from "@pinyinly/lib/invariant"; @@ -34,10 +31,7 @@ import { Text, View } from "react-native"; import Reanimated, { FadeIn, FadeOut } from "react-native-reanimated"; import { useSafeAreaInsets } from "react-native-safe-area-context"; import type { DeepReadonly } from "ts-essentials"; -import { HanziWordRefText } from "./HanziWordRefText"; -import { IconImage } from "./IconImage"; import { PinyinOptionButton } from "./PinyinOptionButton"; -import { Pylymark } from "./Pylymark"; import { QuizDeckToastContainer } from "./QuizDeckToastContainer"; import { QuizFlagText } from "./QuizFlagText"; import { QuizSubmitButton, QuizSubmitButtonState } from "./QuizSubmitButton"; @@ -96,6 +90,11 @@ export function QuizDeckHanziToPinyinQuestion({ expectedAnswer: nonNullable(answers[0]), }); onRating(skillRatings, mistakes); + + // If auto-check is enabled and the answer is correct, advance immediately + if (autoCheck && correct) { + onNext(); + } } else { onNext(); } @@ -103,46 +102,7 @@ export function QuizDeckHanziToPinyinQuestion({ return ( - {grade.correct ? ( - - - Nice! - - ) : ( - <> - - - Incorrect - - - Correct answer: - - - - - - - )} - - ) - } + toast={null} submitButton={ ); -const SkillAnswerText = ({ - skill, -}: { - skill: Skill; - includeAlternatives?: boolean; - hideA?: boolean; - hideB?: boolean; - small?: boolean; -}) => { - switch (skillKindFromSkill(skill)) { - case SkillKind.Deprecated_EnglishToRadical: - case SkillKind.Deprecated_PinyinToRadical: - case SkillKind.Deprecated_RadicalToEnglish: - case SkillKind.Deprecated_RadicalToPinyin: - case SkillKind.Deprecated: - case SkillKind.GlossToHanziWord: - case SkillKind.ImageToHanziWord: - case SkillKind.PinyinFinalAssociation: - case SkillKind.PinyinInitialAssociation: - case SkillKind.PinyinToHanziWord: { - throw new Error( - `ShowSkillAnswer not implemented for ${skillKindFromSkill(skill)}`, - ); - } - case SkillKind.HanziWordToGloss: { - skill = skill as HanziWordSkill; - return ; - } - case SkillKind.HanziWordToPinyinTyped: - case SkillKind.HanziWordToPinyinFinal: - case SkillKind.HanziWordToPinyinInitial: - case SkillKind.HanziWordToPinyinTone: { - skill = skill as HanziWordSkill; - return ; - } - } -}; - -const HanziWordToGlossSkillAnswerText = ({ - skill, -}: { - skill: HanziWordSkill; -}) => { - const hanziWord = hanziWordFromSkill(skill); - - return ( - <> - - - - - ); -}; - -const HanziWordToPinyinSkillAnswerText = ({ - skill, -}: { - skill: HanziWordSkill; -}) => { - const hanziWord = hanziWordFromSkill(skill); - - return ( - - - - ); -}; - const Skeleton = ({ children, toast, diff --git a/projects/app/src/client/ui/QuizDeckOneCorrectPairQuestion.tsx b/projects/app/src/client/ui/QuizDeckOneCorrectPairQuestion.tsx index b0b79a7318..98e6111628 100644 --- a/projects/app/src/client/ui/QuizDeckOneCorrectPairQuestion.tsx +++ b/projects/app/src/client/ui/QuizDeckOneCorrectPairQuestion.tsx @@ -9,16 +9,13 @@ import type { OneCorrectPairQuestionChoice, UnsavedSkillRating, } from "@/data/model"; -import { QuestionFlagKind, SkillKind } from "@/data/model"; +import { QuestionFlagKind } from "@/data/model"; import { oneCorrectPairChoiceText, oneCorrectPairQuestionMistakes, } from "@/data/questions/oneCorrectPair"; -import type { HanziWordSkill, Skill } from "@/data/rizzleSchema"; import { computeSkillRating, - hanziWordFromSkill, - skillKindFromSkill, } from "@/data/skills"; import { longestTextByGraphemes } from "@/util/unicode"; import { invariant } from "@pinyinly/lib/invariant"; @@ -26,10 +23,7 @@ import type { ReactNode } from "react"; import { useState } from "react"; import { Text, View } from "react-native"; import { useSafeAreaInsets } from "react-native-safe-area-context"; -import { HanziWordRefText } from "./HanziWordRefText"; -import { IconImage } from "./IconImage"; import { NewSkillModal } from "./NewSkillModal"; -import { Pylymark } from "./Pylymark"; import { QuizDeckToastContainer } from "./QuizDeckToastContainer"; import { QuizFlagText } from "./QuizFlagText"; import { QuizSubmitButton, QuizSubmitButtonState } from "./QuizSubmitButton"; @@ -94,6 +88,11 @@ export function QuizDeckOneCorrectPairQuestion({ setIsCorrect(isCorrect); onRating(skillRatings, mistakes); + + // If auto-check is enabled and the answer is correct, advance immediately + if (autoCheck && isCorrect) { + onNext(); + } }; const groupAFontSize = textAnswerButtonFontSize( @@ -109,46 +108,7 @@ export function QuizDeckOneCorrectPairQuestion({ return ( - {isCorrect ? ( - - - Nice! - - ) : ( - <> - - - Incorrect - - - Correct answer: - - - - - - - )} - - ) - } + toast={null} submitButton={ { - switch (skillKindFromSkill(skill)) { - case SkillKind.Deprecated_EnglishToRadical: - case SkillKind.Deprecated_PinyinToRadical: - case SkillKind.Deprecated_RadicalToEnglish: - case SkillKind.Deprecated_RadicalToPinyin: - case SkillKind.Deprecated: - case SkillKind.GlossToHanziWord: - case SkillKind.ImageToHanziWord: - case SkillKind.PinyinFinalAssociation: - case SkillKind.PinyinInitialAssociation: - case SkillKind.PinyinToHanziWord: { - throw new Error( - `ShowSkillAnswer not implemented for ${skillKindFromSkill(skill)}`, - ); - } - case SkillKind.HanziWordToGloss: { - skill = skill as HanziWordSkill; - return ; - } - case SkillKind.HanziWordToPinyinTyped: - case SkillKind.HanziWordToPinyinFinal: - case SkillKind.HanziWordToPinyinInitial: - case SkillKind.HanziWordToPinyinTone: { - skill = skill as HanziWordSkill; - return ; - } - } -}; - -const HanziWordToGlossSkillAnswerText = ({ - skill, -}: { - skill: HanziWordSkill; -}) => { - const hanziWord = hanziWordFromSkill(skill); - - return ( - <> - - - - - ); -}; - -const HanziWordToPinyinSkillAnswerText = ({ - skill, -}: { - skill: HanziWordSkill; -}) => { - const hanziWord = hanziWordFromSkill(skill); - - return ( - - - - ); -}; - const Skeleton = ({ children, toast,