Skip to content

Commit

Permalink
add review view, filters and fix toasts
Browse files Browse the repository at this point in the history
  • Loading branch information
mmtftr committed Apr 20, 2024
1 parent 9111ec1 commit 2a39035
Show file tree
Hide file tree
Showing 18 changed files with 919 additions and 121 deletions.
Binary file modified .yarn/install-state.gz
Binary file not shown.
60 changes: 57 additions & 3 deletions app/(tabs)/_layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,20 @@ import React from "react";
import FontAwesome from "@expo/vector-icons/FontAwesome";
import { Tabs } from "expo-router";

import Colors from "@/constants/Colors";
import { useColorScheme } from "@/components/useColorScheme";
import { useClientOnlyValue } from "@/components/useClientOnlyValue";
import { Button } from "tamagui";
import { Button, Heading, useTheme, XStack } from "tamagui";
import { Delete } from "@tamagui/lucide-icons";
import { resetAndReseed } from "../../components/SeedProvider";
import { useSafeAreaInsets } from "react-native-safe-area-context";
import { Pressable } from "react-native";
import Animated, {
useAnimatedStyle,
useSharedValue,
withSpring,
withTiming,
} from "react-native-reanimated";
import { fastSpring } from "@/constants/tamagui";

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

const { top } = useSafeAreaInsets();
const t = useTheme();

return (
<Tabs
screenOptions={{
tabBarActiveTintColor: Colors[colorScheme ?? "light"].tint,
tabBarActiveTintColor: t.blue10.get(),
// Disable the static render of the header on web
// to prevent a hydration error in React Navigation v6.
headerShown: useClientOnlyValue(false, true),
tabBarShowLabel: false,
tabBarButton({ ...props }) {
const pressedIn = useSharedValue(false);

const animatedScale = useAnimatedStyle(() => {
return {
transform: [
{ scale: withSpring(pressedIn.value ? 0.85 : 1, fastSpring) },
],
};
});

return (
<Animated.View
style={[{ flex: 1, flexDirection: "row" }, animatedScale]}
>
<Pressable
onPressIn={() => (pressedIn.value = true)}
onPressOut={() => (pressedIn.value = false)}
{...props}
/>
</Animated.View>
);
},
header(props) {
return (
<XStack
paddingTop={top}
justifyContent="center"
pb="$2"
backgroundColor="$accentBackground"
>
<Heading size="$5">Jellip</Heading>
</XStack>
);
},
}}
>
<Tabs.Screen
Expand Down Expand Up @@ -60,6 +107,13 @@ export default function TabLayout() {
),
}}
/>
<Tabs.Screen
name="settings"
options={{
title: "Settings",
tabBarIcon: ({ color }) => <TabBarIcon name="gear" color={color} />,
}}
/>
</Tabs>
);
}
79 changes: 53 additions & 26 deletions app/(tabs)/question.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { AnimatePresence, View } from "tamagui";
import { AnimatePresence, Heading, Paragraph, View, YStack } from "tamagui";
import { useCallback, useEffect, useState } from "react";
import {
getRandomQuestion,
Expand All @@ -8,6 +8,7 @@ import {
import { useToastController } from "@tamagui/toast";
import * as zod from "zod";
import { QuestionView } from "@/components/QuestionView";
import { settingsStore } from "@/services/store";

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

const [categoryFilter, levelFilter] = settingsStore((state) => [
state.data.categoryFilter,
state.data.levelFilter,
]);

const fetchQuestion = useCallback(
async function () {
setLoading(true);
try {
const question = await getRandomQuestion();
const { categoryFilter, levelFilter } = settingsStore.getState().data;
const question = await getRandomQuestion({
categoryFilter: categoryFilter.length ? categoryFilter : undefined,
levelFilter: levelFilter.length ? levelFilter : undefined,
});

if (!question) {
toast.show("No Question Found", {
type: "error",
message: "No question found. Please try again.",
message: "No question found. Please change your filters.",
});
setQuestion(null);
return;
}
const result = questionSchema.safeParse(question);
if (!result.success) {
toast.show("Invalid Question", {
type: "error",
message:
"Invalid question received. Errors: " + result.error.toString(),
message: "Invalid question received.",
});
return;
}
Expand All @@ -51,14 +62,13 @@ function QuestionManager() {
setLoading(false);
}
},
[setQuestion],
[setQuestion]
);

useEffect(() => {
if (question) return;
setAnswer(null);
fetchQuestion();
}, []);
}, [categoryFilter, levelFilter]);

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

return (
<AnimatePresence>
<View
position="absolute"
top={0}
left={0}
right={0}
bottom={0}
animation="fast"
key={question?.question.toString()}
exitStyle={{ transform: [{ translateX: 200 }], opacity: 0 }}
enterStyle={{ transform: [{ translateX: -200 }], opacity: 0 }}
transform={[{ translateX: 0 }]}
>
<QuestionView
question={question}
answer={answer}
handleAnswer={handleAnswer}
/>
</View>
{question && (
<View
position="absolute"
top={0}
left={0}
right={0}
bottom={0}
animation="fast"
paddingHorizontal="$8"
key={question?.question.toString()}
exitStyle={{ transform: [{ translateX: 200 }], opacity: 0 }}
enterStyle={{ transform: [{ translateX: -200 }], opacity: 0 }}
transform={[{ translateX: 0 }]}
>
<QuestionView
question={question}
answer={answer}
handleAnswer={handleAnswer}
/>
</View>
)}
{!question && !loading && (
<YStack
f={1}
enterStyle={{ opacity: 0 }}
exitStyle={{ opacity: 0 }}
animation="fast"
jc="center"
ai="center"
gap="$2"
>
<Heading>No question found.</Heading>
<Paragraph>Try changing the filters to get a question.</Paragraph>
</YStack>
)}
</AnimatePresence>
);
}
Expand Down
161 changes: 161 additions & 0 deletions app/(tabs)/settings.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
import { getAnswersToday, QuestionWithAnswers } from "@/services/questions";
import React, { useEffect, useMemo } from "react";
import {
YStack,
Heading,
Text,
XStack,
Label,
useTheme,
View,
useMedia,
Paragraph,
Button,
} from "tamagui";
import { answersTodayStore, settingsStore } from "@/services/store";
import { SelectBox } from "../../components/SelectBox";
import { PieChart } from "@/components/PieChart";
import { useRouter } from "expo-router";

const LevelFilter = () => {
const filters = settingsStore((state) => state.data.levelFilter);
const [val, setVal] = React.useState<QuestionWithAnswers["level"] | "all">(
filters.length === 0 ? "all" : filters[0]
);
const items = [
{ name: "all" },
{ name: "N1" },
{ name: "N2" },
{ name: "N3" },
{ name: "N4" },
{ name: "N5" },
];
const name = "Level Filter";
useEffect(() => {
settingsStore.getState().update((state) => {
if (val === "all") {
state.levelFilter = [];
return;
}
state.levelFilter = [val];
});
}, [val]);

return (
<XStack jc="space-between">
<Label>{name}</Label>
<SelectBox
val={val}
// @ts-ignore
setVal={setVal}
name={name}
items={items}
placeholder="Level filter..."
triggerProps={{ width: "50%" }}
/>
</XStack>
);
};

const CategoryFilter = () => {
const filters = settingsStore((state) => state.data.categoryFilter);
const [val, setVal] = React.useState<QuestionWithAnswers["category"] | "all">(
filters.length === 0 ? "all" : filters[0]
);
const items = [
{ name: "all" },
{ name: "vocabulary" },
{ name: "grammar" },
{ name: "kanji" },
];
const name = "Category Filter";
useEffect(() => {
settingsStore.getState().update((state) => {
if (val === "all") {
state.categoryFilter = [];
return;
}
state.categoryFilter = [val];
});
}, [val]);

return (
<XStack jc="space-between">
<Label>{name}</Label>
<SelectBox
val={val}
// @ts-ignore
setVal={setVal}
name={name}
items={items}
placeholder="Category filter..."
triggerProps={{ width: "50%" }}
/>
</XStack>
);
};

const SettingsTab: React.FC = () => {
const theme = useTheme();
const numberOfQuestionsSolvedToday = answersTodayStore((s) => s.data.val);
const [answers, setAnswers] = React.useState<
Awaited<ReturnType<typeof getAnswersToday>>
>([]);
useEffect(() => {
getAnswersToday().then((answers) => {
answersTodayStore.getState().update((state) => {
state.val = answers.length;
});
setAnswers(answers);
});
}, [numberOfQuestionsSolvedToday]);

const correctCount = useMemo(
() =>
answers.filter((s) => s.questions.correctAnswer === s.answers.answer)
.length,
[answers]
);
const incorrectCount = answers.length - correctCount;
const router = useRouter();
return (
<YStack padding="$8" gap="$4">
<Heading>Settings</Heading>
<Text>
Number of questions solved today: {numberOfQuestionsSolvedToday}
</Text>
<CategoryFilter />
<LevelFilter />
<Heading>Correct to Incorrect Ratio</Heading>
<XStack gap="$4">
<View w="50%" aspectRatio={1}>
<PieChart
data={[
{
value: correctCount,
color: theme.green11.get(),
},
{
value: incorrectCount,
color: theme.red11.get(),
},
]}
/>
</View>
<YStack gap="$2">
<Paragraph>
Correct: {correctCount} (
{((correctCount / answers.length) * 100).toFixed(2)}%)
</Paragraph>
<Paragraph>
Incorrect: {incorrectCount} (
{((incorrectCount / answers.length) * 100).toFixed(2)}%)
</Paragraph>
</YStack>
</XStack>
<Button onPress={() => router.push("/review")}>Review</Button>
</YStack>
);
};

export default SettingsTab;
Loading

0 comments on commit 2a39035

Please sign in to comment.