Added answers and question updated

This commit is contained in:
Stepan 2026-01-05 17:00:28 +01:00
parent 8f8bc0f541
commit 626345bc9d
16 changed files with 267 additions and 32 deletions

View File

@ -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;

View File

@ -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";

View File

@ -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';

View File

@ -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 (
<Content>
@ -25,6 +24,9 @@ export default function QuestionsScreen() {
onPress={() => router.push(`/questions/${question.id}`)}
/>
))}
{isLoadingQuestions && Array.from({ length: 5 }).map((_, i) => (
<Question key={i} />
))}
</View>
</Content>

View File

@ -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';

View File

@ -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";

View File

@ -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 (
<>
<Stack.Screen options={{ title: `Question #${id}` }} />
<Content>{isLoading && <Question />}</Content>
</>
);
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 (
<>
<Stack.Screen options={{ title: `Question #${id}` }} />
<Content>
{isLoading && <Skeleton variant="rounded" className="h-32" />}
{ data && <Question question={data} /> }
</Content>
</>
)
}
<Stack.Screen options={{ title: `Question #${id}` }} />
<Content>
<Question question={data} withMeta={true} />
export default QuestionScreen;
<Divider className="mt-5 mb-5" />
<View className="gap-4">
{displayAnswers &&
answers.map((answer) => (
<Answer
key={answer.id}
answer={answer}
isTextType={data.type === "text"}
correctAnswers={data.correct_answers}
displayCorrect={withCorrect}
/>
))}
<Button
action="secondary"
onPress={() => setWithCorrect(!withCorrect)}
>
<ButtonText>Display answers: {withCorrectBtnText}</ButtonText>
</Button>
</View>
</Content>
</>
);
};
export default QuestionScreen;

View File

@ -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 <Skeleton className="w-full h-20" />;
return (
<Pressable onPress={onPress}>
<Panel>
<ThemedText type="meta" className="mb-1">
{question.category.name}
</ThemedText>
<ThemedText style={styles.questionTitle}>{question.title}</ThemedText>
<ThemedText>{question.description}</ThemedText>
<Divider className="mt-2 mb-2" />
<View className="justify-between flex-row">
<ThemedText type="meta">
<FontAwesome5 name="clipboard-list" /> {capitalizeFirst(question.type)}
</ThemedText>
<ThemedText type="meta">
<FontAwesome5 name="dumbbell" className="me-1" />
{question.difficulty}
</ThemedText>
</View>
</Panel>
</Pressable>
)
}
);
};
const styles = StyleSheet.create({
questionTitle: {
fontSize: 18,
fontWeight: "600",
marginBottom: 10
marginBottom: 10,
},
});
export default Question;
export default Question;

148
components/ui/answer.tsx Normal file
View File

@ -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<UserAnsweredType>(() => {
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 <Skeleton className="w-full h-16" />;
return (
<Pressable onPress={onPress}>
<Panel style={stylePanel} className="flex-row">
<View style={styles.answerNumberation}>
{isTextType ? (
<FontAwesome5 name="quote-left" />
) : (
<ThemedText darkColor="#fff" lightColor="#fff">
{answer.id}
</ThemedText>
)}
</View>
<ThemedText lightColor={textColor} darkColor={textColor}>{answer.text}</ThemedText>
</Panel>
</Pressable>
);
};
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;

View File

@ -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<ViewStyle>;
}
const Panel = ({children}: PanelProps) => {
const Panel = ({children, className, style}: PanelProps) => {
const backgroundColor = useThemeColor({ }, 'background');
return (
<View style={[styles.panel, { backgroundColor }]}>
<View style={[styles.panel, { backgroundColor }, style]} className={className}>
{children}
</View>
)

View File

@ -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 (
<Text
@ -26,6 +27,7 @@ export function ThemedText({
type === 'defaultSemiBold' ? styles.defaultSemiBold : undefined,
type === 'subtitle' ? styles.subtitle : undefined,
type === 'link' ? styles.link : undefined,
type === 'meta' ? styles.meta : undefined,
style,
]}
{...rest}
@ -57,4 +59,7 @@ const styles = StyleSheet.create({
fontSize: 16,
color: '#0a7ea4',
},
meta: {
fontSize: 14
}
});

View File

@ -18,6 +18,7 @@ export const Colors = {
errorText: "#cf0404",
tint: tintColorLight,
icon: '#687076',
meta: '#919191',
tabIconDefault: '#687076',
tabIconSelected: tintColorLight,
},
@ -28,6 +29,7 @@ export const Colors = {
errorText: "#cf0404",
tint: tintColorDark,
icon: '#9BA1A6',
meta: '#cfcfcf',
tabIconDefault: '#9BA1A6',
tabIconSelected: tintColorDark,
},

View File

@ -0,0 +1,6 @@
function capitalizeFirst(str: string) {
if (!str) return '';
return str.charAt(0).toUpperCase() + str.slice(1).toLowerCase();
}
export default capitalizeFirst