From 626345bc9d5da9c399b29d8ca8c30e301bce916b Mon Sep 17 00:00:00 2001 From: Stepan Date: Mon, 5 Jan 2026 17:00:28 +0100 Subject: [PATCH] Added answers and question updated --- api/types.ts | 4 +- app/(tabs)/index.tsx | 2 +- app/(tabs)/me.tsx | 2 +- app/(tabs)/questions.tsx | 6 +- app/_layout.tsx | 2 +- app/login.tsx | 2 +- app/questions/[id].tsx | 70 +++++++-- .../auth-provider/context.tsx | 0 .../{ui => providers}/auth-provider/hook.ts | 0 .../{ui => providers}/auth-provider/index.tsx | 0 components/question.tsx | 38 +++-- components/ui/answer.tsx | 148 ++++++++++++++++++ components/ui/panel.tsx | 8 +- components/ui/themed-text.tsx | 9 +- constants/theme.ts | 2 + utils/capitalize-first.ts | 6 + 16 files changed, 267 insertions(+), 32 deletions(-) rename components/{ui => providers}/auth-provider/context.tsx (100%) rename components/{ui => providers}/auth-provider/hook.ts (100%) rename components/{ui => providers}/auth-provider/index.tsx (100%) create mode 100644 components/ui/answer.tsx create mode 100644 utils/capitalize-first.ts diff --git a/api/types.ts b/api/types.ts index 2c4be95..ef7239b 100644 --- a/api/types.ts +++ b/api/types.ts @@ -29,8 +29,8 @@ export interface QuestionResponse { type: QuestionTypes; difficulty: number; - variants: QuestionVariant[]; - correct_answers: number[]; + variants?: QuestionVariant[]; + correct_answers: number[]|string[]; category_id: number; category: CategoryResponse; diff --git a/app/(tabs)/index.tsx b/app/(tabs)/index.tsx index 9b77a9d..fb18439 100644 --- a/app/(tabs)/index.tsx +++ b/app/(tabs)/index.tsx @@ -4,9 +4,9 @@ import { StyleSheet } from "react-native"; import { useQuestions } from "@/api"; import { HelloWave } from "@/components/hello-wave"; import ParallaxScrollView from "@/components/parallax-scroll-view"; +import useAuthContext from "@/components/providers/auth-provider/hook"; import Question from "@/components/question"; import { ThemedView } from "@/components/themed-view"; -import useAuthContext from "@/components/ui/auth-provider/hook"; import { ThemedText } from "@/components/ui/themed-text"; import { router } from "expo-router"; diff --git a/app/(tabs)/me.tsx b/app/(tabs)/me.tsx index 4182983..3720926 100644 --- a/app/(tabs)/me.tsx +++ b/app/(tabs)/me.tsx @@ -1,5 +1,5 @@ -import useAuthContext from '@/components/ui/auth-provider/hook'; +import useAuthContext from '@/components/providers/auth-provider/hook'; import { Button, ButtonText } from '@/components/ui/button'; import Content from '@/components/ui/content'; import { Divider } from '@/components/ui/divider'; diff --git a/app/(tabs)/questions.tsx b/app/(tabs)/questions.tsx index ecc05f1..894f341 100644 --- a/app/(tabs)/questions.tsx +++ b/app/(tabs)/questions.tsx @@ -9,8 +9,7 @@ import { router } from 'expo-router'; export default function QuestionsScreen() { const { data: questions, isLoading: isLoadingQuestions } = useQuestions(); - const questionsLoaded = - !isLoadingQuestions && questions && questions.meta.total > 0; + const questionsLoaded = !isLoadingQuestions && questions && questions.meta.total > 0; return ( @@ -25,6 +24,9 @@ export default function QuestionsScreen() { onPress={() => router.push(`/questions/${question.id}`)} /> ))} + {isLoadingQuestions && Array.from({ length: 5 }).map((_, i) => ( + + ))} diff --git a/app/_layout.tsx b/app/_layout.tsx index b91e533..d6c46de 100644 --- a/app/_layout.tsx +++ b/app/_layout.tsx @@ -10,7 +10,7 @@ import "react-native-reanimated"; import { useColorScheme } from "@/hooks/use-color-scheme"; -import AuthProvider from "@/components/ui/auth-provider"; +import AuthProvider from "@/components/providers/auth-provider"; import { GluestackUIProvider } from "@/components/ui/gluestack-ui-provider"; import "@/global.css"; import { ToastProvider } from 'react-native-toast-notifications'; diff --git a/app/login.tsx b/app/login.tsx index 4561db0..51572d8 100644 --- a/app/login.tsx +++ b/app/login.tsx @@ -1,4 +1,4 @@ -import useAuthContext from "@/components/ui/auth-provider/hook"; +import useAuthContext from "@/components/providers/auth-provider/hook"; import Content from "@/components/ui/content"; import LoginForm, { LoginFormData } from "@/components/ui/login-form"; import Panel from "@/components/ui/panel"; diff --git a/app/questions/[id].tsx b/app/questions/[id].tsx index 3e17747..211b043 100644 --- a/app/questions/[id].tsx +++ b/app/questions/[id].tsx @@ -1,23 +1,73 @@ import { useQuestion } from "@/api"; +import { QuestionTypes, QuestionVariant } from "@/api/types"; import Question from "@/components/question"; +import Answer from "@/components/ui/answer"; +import { Button, ButtonText } from "@/components/ui/button"; import Content from "@/components/ui/content"; -import { Skeleton } from "@/components/ui/skeleton"; +import { Divider } from "@/components/ui/divider"; import { Stack, useLocalSearchParams } from "expo-router"; +import { useState } from "react"; +import { View } from "react-native"; const QuestionScreen = () => { const { id: idParam } = useLocalSearchParams<{ id: string }>(); const id = +idParam; const { data, isLoading } = useQuestion(id); + const [withCorrect, setWithCorrect] = useState(false); + + if (!data) + return ( + <> + + {isLoading && } + + ); + + const answers: QuestionVariant[] = data.variants?.length + ? data.variants + : [ + { + id: 1, + text: data.correct_answers[0] as string, + }, + ]; + + const displayAnswers = + (data.type === QuestionTypes.Text && withCorrect) || + data.type !== QuestionTypes.Text; + + const withCorrectBtnText = withCorrect ? "ON" : "OFF"; return ( <> - - - {isLoading && } - { data && } - - - ) -} + + + -export default QuestionScreen; \ No newline at end of file + + + + {displayAnswers && + answers.map((answer) => ( + + ))} + + + + + + ); +}; + +export default QuestionScreen; diff --git a/components/ui/auth-provider/context.tsx b/components/providers/auth-provider/context.tsx similarity index 100% rename from components/ui/auth-provider/context.tsx rename to components/providers/auth-provider/context.tsx diff --git a/components/ui/auth-provider/hook.ts b/components/providers/auth-provider/hook.ts similarity index 100% rename from components/ui/auth-provider/hook.ts rename to components/providers/auth-provider/hook.ts diff --git a/components/ui/auth-provider/index.tsx b/components/providers/auth-provider/index.tsx similarity index 100% rename from components/ui/auth-provider/index.tsx rename to components/providers/auth-provider/index.tsx diff --git a/components/question.tsx b/components/question.tsx index e5c6c43..816ae3d 100644 --- a/components/question.tsx +++ b/components/question.tsx @@ -1,32 +1,52 @@ import { QuestionResponse } from "@/api/types"; -import { Pressable, StyleSheet } from "react-native"; +import capitalizeFirst from "@/utils/capitalize-first"; +import { FontAwesome5 } from "@expo/vector-icons"; +import { Pressable, StyleSheet, View } from "react-native"; +import { Divider } from "./ui/divider"; import Panel from "./ui/panel"; +import { Skeleton } from "./ui/skeleton"; import { ThemedText } from "./ui/themed-text"; interface QuestionProps { - question: QuestionResponse; + question?: QuestionResponse; onPress?: () => void; + withMeta?: boolean; } -const Question = ({ question, onPress }: QuestionProps) => { +const Question = ({ question, onPress, withMeta = false }: QuestionProps) => { + if (!question) return ; + return ( + + {question.category.name} + {question.title} {question.description} + + + + + + {capitalizeFirst(question.type)} + + + + {question.difficulty} + + - ) -} + ); +}; const styles = StyleSheet.create({ questionTitle: { fontSize: 18, fontWeight: "600", - marginBottom: 10 + marginBottom: 10, }, }); - - -export default Question; \ No newline at end of file +export default Question; diff --git a/components/ui/answer.tsx b/components/ui/answer.tsx new file mode 100644 index 0000000..8cbaa29 --- /dev/null +++ b/components/ui/answer.tsx @@ -0,0 +1,148 @@ +import { QuestionVariant } from "@/api/types"; +import { FontAwesome5 } from "@expo/vector-icons"; +import { useMemo } from "react"; +import { Pressable, StyleSheet, View } from "react-native"; +import Panel from "./panel"; +import { Skeleton } from "./skeleton"; +import { ThemedText } from "./themed-text"; + +interface AnswerProps { + answer?: QuestionVariant; + isTextType?: boolean; + + userAnswers?: string[] | number[]; + correctAnswers?: string[] | number[]; + + isCorrectUserAnswer?: boolean; + + displayCorrect?: boolean; + displayUserAnswer?: boolean; + + onPress?: () => void; +} +type UserAnsweredType = "correct"|"incorrect"|"non_answered"; + +const Answer = ({ + answer, + onPress, + userAnswers, + correctAnswers, + + isTextType = false, + isCorrectUserAnswer = false, + displayCorrect = false, + displayUserAnswer = false +}: AnswerProps) => { + // Check if user answered on this answer variation + const isAnsweredByUser = useMemo(() => { + if(!userAnswers || !answer) return false; + + if(isTextType) { + return (userAnswers[0] as string).length > 0; + } + return (userAnswers as number[]).includes(answer.id); + }, [userAnswers, answer, isTextType]); + + // Check if this answer variation is correct + const isCorrectAnswer = useMemo(() => { + if(!correctAnswers || !answer) return false; + + if(isTextType) { + return true; + } + + return (correctAnswers as number[]).includes(answer.id); + }, [correctAnswers, isTextType, answer]); + + // Check if user answered correct on this answer variation + const userAnsweredCorrectly = useMemo(() => { + if(isTextType) return isCorrectUserAnswer ? "correct" : "incorrect"; + + if(!isAnsweredByUser && isCorrectAnswer) + return "non_answered"; + + return isCorrectUserAnswer ? "correct" : "incorrect"; + }, [isTextType, isAnsweredByUser, isCorrectAnswer, isCorrectUserAnswer]); + + const stylePanel = useMemo(() => { + let style = undefined; + + if(displayCorrect && isCorrectAnswer && !isTextType) + style = styles.correctAnswer; + + if(displayUserAnswer && isAnsweredByUser && !isTextType) + style = styles.userAnsweredAnswer; + + if(displayUserAnswer && displayCorrect) { + switch (userAnsweredCorrectly) { + case 'correct': + if(isAnsweredByUser) + style = styles.correctAnswer; + break; + case 'incorrect': + style = styles.incorrectAnswer; + break; + case 'non_answered': + style = styles.nonAnsweredAnswer; + break; + } + } + + return style; + }, [displayCorrect, displayUserAnswer, isAnsweredByUser, isCorrectAnswer, isTextType, userAnsweredCorrectly]); + const textColor = useMemo(() => { + if(stylePanel) + return "#ffffff"; + else + return undefined; + }, [stylePanel]); + + if (!answer) return ; + + return ( + + + + {isTextType ? ( + + ) : ( + + {answer.id} + + )} + + {answer.text} + + + ); +}; + +const styles = StyleSheet.create({ + answerNumberation: { + width: 25, + height: 25, + alignItems: "center", + justifyContent: "center", + borderRadius: 5, + marginRight: 10, + backgroundColor: "#bdbdbd", + }, + correctAnswer: { + backgroundColor: "#2ab300" + }, + incorrectAnswer: { + backgroundColor: "#c40404" + }, + nonAnsweredAnswer: { + borderWidth: 1, + borderStyle: "solid", + borderColor: "#2ab300" + }, + userAnsweredAnswer: { + borderWidth: 1, + borderStyle: "solid", + borderColor: "#2cb3f2" + } +}); + +export default Answer; diff --git a/components/ui/panel.tsx b/components/ui/panel.tsx index 0c09dd1..fac6de0 100644 --- a/components/ui/panel.tsx +++ b/components/ui/panel.tsx @@ -1,16 +1,18 @@ import { useThemeColor } from "@/hooks/use-theme-color"; import { ReactNode } from "react"; -import { StyleSheet, View } from "react-native"; +import { StyleProp, StyleSheet, View, ViewStyle } from "react-native"; interface PanelProps { children?: ReactNode; + className?: string; + style?: StyleProp; } -const Panel = ({children}: PanelProps) => { +const Panel = ({children, className, style}: PanelProps) => { const backgroundColor = useThemeColor({ }, 'background'); return ( - + {children} ) diff --git a/components/ui/themed-text.tsx b/components/ui/themed-text.tsx index d79d0a1..91bed6e 100644 --- a/components/ui/themed-text.tsx +++ b/components/ui/themed-text.tsx @@ -5,7 +5,7 @@ import { useThemeColor } from '@/hooks/use-theme-color'; export type ThemedTextProps = TextProps & { lightColor?: string; darkColor?: string; - type?: 'default' | 'title' | 'defaultSemiBold' | 'subtitle' | 'link'; + type?: 'default' | 'title' | 'defaultSemiBold' | 'subtitle' | 'link' | 'meta'; }; export function ThemedText({ @@ -15,7 +15,8 @@ export function ThemedText({ type = 'default', ...rest }: ThemedTextProps) { - const color = useThemeColor({ light: lightColor, dark: darkColor }, 'text'); + const colorProperty = type === 'meta' ? 'meta' : 'text'; + const color = useThemeColor({ light: lightColor, dark: darkColor }, colorProperty); return (