Skip to content
Draft
Show file tree
Hide file tree
Changes from all 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
66 changes: 66 additions & 0 deletions projects/app/src/client/ui/QuizDeck.tsx
Original file line number Diff line number Diff line change
@@ -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";
Expand Down Expand Up @@ -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";
Expand All @@ -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
Expand All @@ -67,6 +77,10 @@ export const QuizDeck = ({ className }: { className?: string }) => {
const reviewQueue = nextQuestionQuery.data?.reviewQueue ?? null;

const [question, setQuestion] = useState<Question>();
const [toastState, setToastState] = useState<{
correct: boolean;
show: boolean;
} | null>(null);

useEffect(() => {
if (question == null && nextQuestion != null) {
Expand All @@ -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(
Expand All @@ -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 () => {
Expand Down Expand Up @@ -289,6 +314,47 @@ export const QuizDeck = ({ className }: { className?: string }) => {
</Stack.Navigator>
</NavigationContainer>
</NavigationIndependentTree>

{/* Toast shown at deck level */}
{toastState?.show === true ? (
<QuizDeckToastContainer>
<View
className={`
flex-1 gap-[12px] overflow-hidden bg-fg-bg10 px-4 pt-3 pb-safe-offset-[84px]

lg:mb-2 lg:rounded-xl

${toastState.correct ? `theme-success` : `theme-danger`}
`}
>
{toastState.correct ? (
<>
<View className="flex-row items-center gap-[8px]">
<IconImage
size={32}
source={require(`@/assets/icons/check-circled-filled.svg`)}
/>
<Text className="text-2xl font-bold text-fg">Nice!</Text>
</View>
<Delay
ms={1000}
action={() => {
setToastState(null);
}}
/>
</>
) : (
<View className="flex-row items-center gap-[8px]">
<IconImage
size={32}
source={require(`@/assets/icons/close-circled-filled.svg`)}
/>
<Text className="text-2xl font-bold text-fg">Incorrect</Text>
</View>
)}
</View>
</QuizDeckToastContainer>
) : null}
</View>
);
};
Expand Down
120 changes: 6 additions & 114 deletions projects/app/src/client/ui/QuizDeckHanziToPinyinQuestion.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ import type {
PinyinPronunciation,
UnsavedSkillRating,
} from "@/data/model";
import { SkillKind } from "@/data/model";
import type {
PinyinSyllableSuggestion,
PinyinSyllableSuggestions,
Expand All @@ -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";
Expand All @@ -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";
Expand Down Expand Up @@ -96,53 +90,19 @@ 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();
}
};

return (
<Skeleton
toast={
grade == null ? null : (
<View
className={`
flex-1 gap-[12px] overflow-hidden bg-fg-bg10 px-4 pt-3 pb-safe-offset-[84px]

lg:mb-2 lg:rounded-xl

${grade.correct ? `theme-success` : `theme-danger`}
`}
>
{grade.correct ? (
<View className="flex-row items-center gap-[8px]">
<IconImage
size={32}
source={require(`@/assets/icons/check-circled-filled.svg`)}
/>
<Text className="text-2xl font-bold text-fg">Nice!</Text>
</View>
) : (
<>
<View className="flex-row items-center gap-[8px]">
<IconImage
size={32}
source={require(`@/assets/icons/close-circled-filled.svg`)}
/>
<Text className="text-2xl font-bold text-fg">Incorrect</Text>
</View>
<Text className="text-xl/none font-medium text-fg">
Correct answer:
</Text>

<Text className="text-fg">
<SkillAnswerText skill={skill} includeAlternatives />
</Text>
</>
)}
</View>
)
}
toast={null}
submitButton={
<QuizSubmitButton
autoFocus={grade != null}
Expand Down Expand Up @@ -324,74 +284,6 @@ const hiddenPlaceholderOptions = (
</>
);

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 <HanziWordToGlossSkillAnswerText skill={skill} />;
}
case SkillKind.HanziWordToPinyinTyped:
case SkillKind.HanziWordToPinyinFinal:
case SkillKind.HanziWordToPinyinInitial:
case SkillKind.HanziWordToPinyinTone: {
skill = skill as HanziWordSkill;
return <HanziWordToPinyinSkillAnswerText skill={skill} />;
}
}
};

const HanziWordToGlossSkillAnswerText = ({
skill,
}: {
skill: HanziWordSkill;
}) => {
const hanziWord = hanziWordFromSkill(skill);

return (
<>
<Text className="pyly-body-2xl">
<Pylymark source={`{${hanziWord}}`} />
</Text>
</>
);
};

const HanziWordToPinyinSkillAnswerText = ({
skill,
}: {
skill: HanziWordSkill;
}) => {
const hanziWord = hanziWordFromSkill(skill);

return (
<Text className="pyly-body-2xl">
<HanziWordRefText hanziWord={hanziWord} showPinyin />
</Text>
);
};

const Skeleton = ({
children,
toast,
Expand Down
Loading