Skip to content

Commit 2a39035

Browse files
committed
add review view, filters and fix toasts
1 parent 9111ec1 commit 2a39035

18 files changed

+919
-121
lines changed

.yarn/install-state.gz

50.8 KB
Binary file not shown.

app/(tabs)/_layout.tsx

+57-3
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,20 @@ import React from "react";
22
import FontAwesome from "@expo/vector-icons/FontAwesome";
33
import { Tabs } from "expo-router";
44

5-
import Colors from "@/constants/Colors";
65
import { useColorScheme } from "@/components/useColorScheme";
76
import { useClientOnlyValue } from "@/components/useClientOnlyValue";
8-
import { Button } from "tamagui";
7+
import { Button, Heading, useTheme, XStack } from "tamagui";
98
import { Delete } from "@tamagui/lucide-icons";
109
import { resetAndReseed } from "../../components/SeedProvider";
10+
import { useSafeAreaInsets } from "react-native-safe-area-context";
11+
import { Pressable } from "react-native";
12+
import Animated, {
13+
useAnimatedStyle,
14+
useSharedValue,
15+
withSpring,
16+
withTiming,
17+
} from "react-native-reanimated";
18+
import { fastSpring } from "@/constants/tamagui";
1119

1220
// You can explore the built-in icon families and icons on the web at https://icons.expo.fyi/
1321
function TabBarIcon(props: {
@@ -20,13 +28,52 @@ function TabBarIcon(props: {
2028
export default function TabLayout() {
2129
const colorScheme = useColorScheme();
2230

31+
const { top } = useSafeAreaInsets();
32+
const t = useTheme();
33+
2334
return (
2435
<Tabs
2536
screenOptions={{
26-
tabBarActiveTintColor: Colors[colorScheme ?? "light"].tint,
37+
tabBarActiveTintColor: t.blue10.get(),
2738
// Disable the static render of the header on web
2839
// to prevent a hydration error in React Navigation v6.
2940
headerShown: useClientOnlyValue(false, true),
41+
tabBarShowLabel: false,
42+
tabBarButton({ ...props }) {
43+
const pressedIn = useSharedValue(false);
44+
45+
const animatedScale = useAnimatedStyle(() => {
46+
return {
47+
transform: [
48+
{ scale: withSpring(pressedIn.value ? 0.85 : 1, fastSpring) },
49+
],
50+
};
51+
});
52+
53+
return (
54+
<Animated.View
55+
style={[{ flex: 1, flexDirection: "row" }, animatedScale]}
56+
>
57+
<Pressable
58+
onPressIn={() => (pressedIn.value = true)}
59+
onPressOut={() => (pressedIn.value = false)}
60+
{...props}
61+
/>
62+
</Animated.View>
63+
);
64+
},
65+
header(props) {
66+
return (
67+
<XStack
68+
paddingTop={top}
69+
justifyContent="center"
70+
pb="$2"
71+
backgroundColor="$accentBackground"
72+
>
73+
<Heading size="$5">Jellip</Heading>
74+
</XStack>
75+
);
76+
},
3077
}}
3178
>
3279
<Tabs.Screen
@@ -60,6 +107,13 @@ export default function TabLayout() {
60107
),
61108
}}
62109
/>
110+
<Tabs.Screen
111+
name="settings"
112+
options={{
113+
title: "Settings",
114+
tabBarIcon: ({ color }) => <TabBarIcon name="gear" color={color} />,
115+
}}
116+
/>
63117
</Tabs>
64118
);
65119
}

app/(tabs)/question.tsx

+53-26
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { AnimatePresence, View } from "tamagui";
1+
import { AnimatePresence, Heading, Paragraph, View, YStack } from "tamagui";
22
import { useCallback, useEffect, useState } from "react";
33
import {
44
getRandomQuestion,
@@ -8,6 +8,7 @@ import {
88
import { useToastController } from "@tamagui/toast";
99
import * as zod from "zod";
1010
import { QuestionView } from "@/components/QuestionView";
11+
import { settingsStore } from "@/services/store";
1112

1213
const questionSchema = zod.object({
1314
id: zod.number(),
@@ -24,24 +25,34 @@ function QuestionManager() {
2425
const [loading, setLoading] = useState(false);
2526
const toast = useToastController();
2627

28+
const [categoryFilter, levelFilter] = settingsStore((state) => [
29+
state.data.categoryFilter,
30+
state.data.levelFilter,
31+
]);
32+
2733
const fetchQuestion = useCallback(
2834
async function () {
2935
setLoading(true);
3036
try {
31-
const question = await getRandomQuestion();
37+
const { categoryFilter, levelFilter } = settingsStore.getState().data;
38+
const question = await getRandomQuestion({
39+
categoryFilter: categoryFilter.length ? categoryFilter : undefined,
40+
levelFilter: levelFilter.length ? levelFilter : undefined,
41+
});
3242

3343
if (!question) {
3444
toast.show("No Question Found", {
3545
type: "error",
36-
message: "No question found. Please try again.",
46+
message: "No question found. Please change your filters.",
3747
});
48+
setQuestion(null);
49+
return;
3850
}
3951
const result = questionSchema.safeParse(question);
4052
if (!result.success) {
4153
toast.show("Invalid Question", {
4254
type: "error",
43-
message:
44-
"Invalid question received. Errors: " + result.error.toString(),
55+
message: "Invalid question received.",
4556
});
4657
return;
4758
}
@@ -51,14 +62,13 @@ function QuestionManager() {
5162
setLoading(false);
5263
}
5364
},
54-
[setQuestion],
65+
[setQuestion]
5566
);
5667

5768
useEffect(() => {
58-
if (question) return;
5969
setAnswer(null);
6070
fetchQuestion();
61-
}, []);
71+
}, [categoryFilter, levelFilter]);
6272

6373
const handleAnswer = async (answerId: number) => {
6474
if (answer !== null) {
@@ -74,24 +84,41 @@ function QuestionManager() {
7484

7585
return (
7686
<AnimatePresence>
77-
<View
78-
position="absolute"
79-
top={0}
80-
left={0}
81-
right={0}
82-
bottom={0}
83-
animation="fast"
84-
key={question?.question.toString()}
85-
exitStyle={{ transform: [{ translateX: 200 }], opacity: 0 }}
86-
enterStyle={{ transform: [{ translateX: -200 }], opacity: 0 }}
87-
transform={[{ translateX: 0 }]}
88-
>
89-
<QuestionView
90-
question={question}
91-
answer={answer}
92-
handleAnswer={handleAnswer}
93-
/>
94-
</View>
87+
{question && (
88+
<View
89+
position="absolute"
90+
top={0}
91+
left={0}
92+
right={0}
93+
bottom={0}
94+
animation="fast"
95+
paddingHorizontal="$8"
96+
key={question?.question.toString()}
97+
exitStyle={{ transform: [{ translateX: 200 }], opacity: 0 }}
98+
enterStyle={{ transform: [{ translateX: -200 }], opacity: 0 }}
99+
transform={[{ translateX: 0 }]}
100+
>
101+
<QuestionView
102+
question={question}
103+
answer={answer}
104+
handleAnswer={handleAnswer}
105+
/>
106+
</View>
107+
)}
108+
{!question && !loading && (
109+
<YStack
110+
f={1}
111+
enterStyle={{ opacity: 0 }}
112+
exitStyle={{ opacity: 0 }}
113+
animation="fast"
114+
jc="center"
115+
ai="center"
116+
gap="$2"
117+
>
118+
<Heading>No question found.</Heading>
119+
<Paragraph>Try changing the filters to get a question.</Paragraph>
120+
</YStack>
121+
)}
95122
</AnimatePresence>
96123
);
97124
}

app/(tabs)/settings.tsx

+161
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,161 @@
1+
import { getAnswersToday, QuestionWithAnswers } from "@/services/questions";
2+
import React, { useEffect, useMemo } from "react";
3+
import {
4+
YStack,
5+
Heading,
6+
Text,
7+
XStack,
8+
Label,
9+
useTheme,
10+
View,
11+
useMedia,
12+
Paragraph,
13+
Button,
14+
} from "tamagui";
15+
import { answersTodayStore, settingsStore } from "@/services/store";
16+
import { SelectBox } from "../../components/SelectBox";
17+
import { PieChart } from "@/components/PieChart";
18+
import { useRouter } from "expo-router";
19+
20+
const LevelFilter = () => {
21+
const filters = settingsStore((state) => state.data.levelFilter);
22+
const [val, setVal] = React.useState<QuestionWithAnswers["level"] | "all">(
23+
filters.length === 0 ? "all" : filters[0]
24+
);
25+
const items = [
26+
{ name: "all" },
27+
{ name: "N1" },
28+
{ name: "N2" },
29+
{ name: "N3" },
30+
{ name: "N4" },
31+
{ name: "N5" },
32+
];
33+
const name = "Level Filter";
34+
useEffect(() => {
35+
settingsStore.getState().update((state) => {
36+
if (val === "all") {
37+
state.levelFilter = [];
38+
return;
39+
}
40+
state.levelFilter = [val];
41+
});
42+
}, [val]);
43+
44+
return (
45+
<XStack jc="space-between">
46+
<Label>{name}</Label>
47+
<SelectBox
48+
val={val}
49+
// @ts-ignore
50+
setVal={setVal}
51+
name={name}
52+
items={items}
53+
placeholder="Level filter..."
54+
triggerProps={{ width: "50%" }}
55+
/>
56+
</XStack>
57+
);
58+
};
59+
60+
const CategoryFilter = () => {
61+
const filters = settingsStore((state) => state.data.categoryFilter);
62+
const [val, setVal] = React.useState<QuestionWithAnswers["category"] | "all">(
63+
filters.length === 0 ? "all" : filters[0]
64+
);
65+
const items = [
66+
{ name: "all" },
67+
{ name: "vocabulary" },
68+
{ name: "grammar" },
69+
{ name: "kanji" },
70+
];
71+
const name = "Category Filter";
72+
useEffect(() => {
73+
settingsStore.getState().update((state) => {
74+
if (val === "all") {
75+
state.categoryFilter = [];
76+
return;
77+
}
78+
state.categoryFilter = [val];
79+
});
80+
}, [val]);
81+
82+
return (
83+
<XStack jc="space-between">
84+
<Label>{name}</Label>
85+
<SelectBox
86+
val={val}
87+
// @ts-ignore
88+
setVal={setVal}
89+
name={name}
90+
items={items}
91+
placeholder="Category filter..."
92+
triggerProps={{ width: "50%" }}
93+
/>
94+
</XStack>
95+
);
96+
};
97+
98+
const SettingsTab: React.FC = () => {
99+
const theme = useTheme();
100+
const numberOfQuestionsSolvedToday = answersTodayStore((s) => s.data.val);
101+
const [answers, setAnswers] = React.useState<
102+
Awaited<ReturnType<typeof getAnswersToday>>
103+
>([]);
104+
useEffect(() => {
105+
getAnswersToday().then((answers) => {
106+
answersTodayStore.getState().update((state) => {
107+
state.val = answers.length;
108+
});
109+
setAnswers(answers);
110+
});
111+
}, [numberOfQuestionsSolvedToday]);
112+
113+
const correctCount = useMemo(
114+
() =>
115+
answers.filter((s) => s.questions.correctAnswer === s.answers.answer)
116+
.length,
117+
[answers]
118+
);
119+
const incorrectCount = answers.length - correctCount;
120+
const router = useRouter();
121+
return (
122+
<YStack padding="$8" gap="$4">
123+
<Heading>Settings</Heading>
124+
<Text>
125+
Number of questions solved today: {numberOfQuestionsSolvedToday}
126+
</Text>
127+
<CategoryFilter />
128+
<LevelFilter />
129+
<Heading>Correct to Incorrect Ratio</Heading>
130+
<XStack gap="$4">
131+
<View w="50%" aspectRatio={1}>
132+
<PieChart
133+
data={[
134+
{
135+
value: correctCount,
136+
color: theme.green11.get(),
137+
},
138+
{
139+
value: incorrectCount,
140+
color: theme.red11.get(),
141+
},
142+
]}
143+
/>
144+
</View>
145+
<YStack gap="$2">
146+
<Paragraph>
147+
Correct: {correctCount} (
148+
{((correctCount / answers.length) * 100).toFixed(2)}%)
149+
</Paragraph>
150+
<Paragraph>
151+
Incorrect: {incorrectCount} (
152+
{((incorrectCount / answers.length) * 100).toFixed(2)}%)
153+
</Paragraph>
154+
</YStack>
155+
</XStack>
156+
<Button onPress={() => router.push("/review")}>Review</Button>
157+
</YStack>
158+
);
159+
};
160+
161+
export default SettingsTab;

0 commit comments

Comments
 (0)