Added answers and question updated
This commit is contained in:
parent
8f8bc0f541
commit
626345bc9d
@ -29,8 +29,8 @@ export interface QuestionResponse {
|
|||||||
type: QuestionTypes;
|
type: QuestionTypes;
|
||||||
difficulty: number;
|
difficulty: number;
|
||||||
|
|
||||||
variants: QuestionVariant[];
|
variants?: QuestionVariant[];
|
||||||
correct_answers: number[];
|
correct_answers: number[]|string[];
|
||||||
|
|
||||||
category_id: number;
|
category_id: number;
|
||||||
category: CategoryResponse;
|
category: CategoryResponse;
|
||||||
|
|||||||
@ -4,9 +4,9 @@ import { StyleSheet } from "react-native";
|
|||||||
import { useQuestions } from "@/api";
|
import { useQuestions } from "@/api";
|
||||||
import { HelloWave } from "@/components/hello-wave";
|
import { HelloWave } from "@/components/hello-wave";
|
||||||
import ParallaxScrollView from "@/components/parallax-scroll-view";
|
import ParallaxScrollView from "@/components/parallax-scroll-view";
|
||||||
|
import useAuthContext from "@/components/providers/auth-provider/hook";
|
||||||
import Question from "@/components/question";
|
import Question from "@/components/question";
|
||||||
import { ThemedView } from "@/components/themed-view";
|
import { ThemedView } from "@/components/themed-view";
|
||||||
import useAuthContext from "@/components/ui/auth-provider/hook";
|
|
||||||
import { ThemedText } from "@/components/ui/themed-text";
|
import { ThemedText } from "@/components/ui/themed-text";
|
||||||
import { router } from "expo-router";
|
import { router } from "expo-router";
|
||||||
|
|
||||||
|
|||||||
@ -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 { Button, ButtonText } from '@/components/ui/button';
|
||||||
import Content from '@/components/ui/content';
|
import Content from '@/components/ui/content';
|
||||||
import { Divider } from '@/components/ui/divider';
|
import { Divider } from '@/components/ui/divider';
|
||||||
|
|||||||
@ -9,8 +9,7 @@ import { router } from 'expo-router';
|
|||||||
export default function QuestionsScreen() {
|
export default function QuestionsScreen() {
|
||||||
const { data: questions, isLoading: isLoadingQuestions } = useQuestions();
|
const { data: questions, isLoading: isLoadingQuestions } = useQuestions();
|
||||||
|
|
||||||
const questionsLoaded =
|
const questionsLoaded = !isLoadingQuestions && questions && questions.meta.total > 0;
|
||||||
!isLoadingQuestions && questions && questions.meta.total > 0;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Content>
|
<Content>
|
||||||
@ -25,6 +24,9 @@ export default function QuestionsScreen() {
|
|||||||
onPress={() => router.push(`/questions/${question.id}`)}
|
onPress={() => router.push(`/questions/${question.id}`)}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
|
{isLoadingQuestions && Array.from({ length: 5 }).map((_, i) => (
|
||||||
|
<Question key={i} />
|
||||||
|
))}
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
</Content>
|
</Content>
|
||||||
|
|||||||
@ -10,7 +10,7 @@ import "react-native-reanimated";
|
|||||||
|
|
||||||
import { useColorScheme } from "@/hooks/use-color-scheme";
|
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 { GluestackUIProvider } from "@/components/ui/gluestack-ui-provider";
|
||||||
import "@/global.css";
|
import "@/global.css";
|
||||||
import { ToastProvider } from 'react-native-toast-notifications';
|
import { ToastProvider } from 'react-native-toast-notifications';
|
||||||
|
|||||||
@ -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 Content from "@/components/ui/content";
|
||||||
import LoginForm, { LoginFormData } from "@/components/ui/login-form";
|
import LoginForm, { LoginFormData } from "@/components/ui/login-form";
|
||||||
import Panel from "@/components/ui/panel";
|
import Panel from "@/components/ui/panel";
|
||||||
|
|||||||
@ -1,23 +1,73 @@
|
|||||||
import { useQuestion } from "@/api";
|
import { useQuestion } from "@/api";
|
||||||
|
import { QuestionTypes, QuestionVariant } from "@/api/types";
|
||||||
import Question from "@/components/question";
|
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 Content from "@/components/ui/content";
|
||||||
import { Skeleton } from "@/components/ui/skeleton";
|
import { Divider } from "@/components/ui/divider";
|
||||||
import { Stack, useLocalSearchParams } from "expo-router";
|
import { Stack, useLocalSearchParams } from "expo-router";
|
||||||
|
import { useState } from "react";
|
||||||
|
import { View } from "react-native";
|
||||||
|
|
||||||
const QuestionScreen = () => {
|
const QuestionScreen = () => {
|
||||||
const { id: idParam } = useLocalSearchParams<{ id: string }>();
|
const { id: idParam } = useLocalSearchParams<{ id: string }>();
|
||||||
const id = +idParam;
|
const id = +idParam;
|
||||||
const { data, isLoading } = useQuestion(id);
|
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 (
|
return (
|
||||||
<>
|
<>
|
||||||
<Stack.Screen options={{ title: `Question #${id}` }} />
|
<Stack.Screen options={{ title: `Question #${id}` }} />
|
||||||
<Content>
|
<Content>
|
||||||
{isLoading && <Skeleton variant="rounded" className="h-32" />}
|
<Question question={data} withMeta={true} />
|
||||||
{ data && <Question question={data} /> }
|
|
||||||
</Content>
|
<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;
|
export default QuestionScreen;
|
||||||
@ -1,32 +1,52 @@
|
|||||||
import { QuestionResponse } from "@/api/types";
|
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 Panel from "./ui/panel";
|
||||||
|
import { Skeleton } from "./ui/skeleton";
|
||||||
import { ThemedText } from "./ui/themed-text";
|
import { ThemedText } from "./ui/themed-text";
|
||||||
|
|
||||||
interface QuestionProps {
|
interface QuestionProps {
|
||||||
question: QuestionResponse;
|
question?: QuestionResponse;
|
||||||
onPress?: () => void;
|
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 (
|
return (
|
||||||
<Pressable onPress={onPress}>
|
<Pressable onPress={onPress}>
|
||||||
<Panel>
|
<Panel>
|
||||||
|
<ThemedText type="meta" className="mb-1">
|
||||||
|
{question.category.name}
|
||||||
|
</ThemedText>
|
||||||
<ThemedText style={styles.questionTitle}>{question.title}</ThemedText>
|
<ThemedText style={styles.questionTitle}>{question.title}</ThemedText>
|
||||||
<ThemedText>{question.description}</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>
|
</Panel>
|
||||||
</Pressable>
|
</Pressable>
|
||||||
)
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
const styles = StyleSheet.create({
|
||||||
questionTitle: {
|
questionTitle: {
|
||||||
fontSize: 18,
|
fontSize: 18,
|
||||||
fontWeight: "600",
|
fontWeight: "600",
|
||||||
marginBottom: 10
|
marginBottom: 10,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
export default Question;
|
export default Question;
|
||||||
148
components/ui/answer.tsx
Normal file
148
components/ui/answer.tsx
Normal 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;
|
||||||
@ -1,16 +1,18 @@
|
|||||||
import { useThemeColor } from "@/hooks/use-theme-color";
|
import { useThemeColor } from "@/hooks/use-theme-color";
|
||||||
import { ReactNode } from "react";
|
import { ReactNode } from "react";
|
||||||
import { StyleSheet, View } from "react-native";
|
import { StyleProp, StyleSheet, View, ViewStyle } from "react-native";
|
||||||
|
|
||||||
interface PanelProps {
|
interface PanelProps {
|
||||||
children?: ReactNode;
|
children?: ReactNode;
|
||||||
|
className?: string;
|
||||||
|
style?: StyleProp<ViewStyle>;
|
||||||
}
|
}
|
||||||
|
|
||||||
const Panel = ({children}: PanelProps) => {
|
const Panel = ({children, className, style}: PanelProps) => {
|
||||||
const backgroundColor = useThemeColor({ }, 'background');
|
const backgroundColor = useThemeColor({ }, 'background');
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View style={[styles.panel, { backgroundColor }]}>
|
<View style={[styles.panel, { backgroundColor }, style]} className={className}>
|
||||||
{children}
|
{children}
|
||||||
</View>
|
</View>
|
||||||
)
|
)
|
||||||
|
|||||||
@ -5,7 +5,7 @@ import { useThemeColor } from '@/hooks/use-theme-color';
|
|||||||
export type ThemedTextProps = TextProps & {
|
export type ThemedTextProps = TextProps & {
|
||||||
lightColor?: string;
|
lightColor?: string;
|
||||||
darkColor?: string;
|
darkColor?: string;
|
||||||
type?: 'default' | 'title' | 'defaultSemiBold' | 'subtitle' | 'link';
|
type?: 'default' | 'title' | 'defaultSemiBold' | 'subtitle' | 'link' | 'meta';
|
||||||
};
|
};
|
||||||
|
|
||||||
export function ThemedText({
|
export function ThemedText({
|
||||||
@ -15,7 +15,8 @@ export function ThemedText({
|
|||||||
type = 'default',
|
type = 'default',
|
||||||
...rest
|
...rest
|
||||||
}: ThemedTextProps) {
|
}: ThemedTextProps) {
|
||||||
const color = useThemeColor({ light: lightColor, dark: darkColor }, 'text');
|
const colorProperty = type === 'meta' ? 'meta' : 'text';
|
||||||
|
const color = useThemeColor({ light: lightColor, dark: darkColor }, colorProperty);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Text
|
<Text
|
||||||
@ -26,6 +27,7 @@ export function ThemedText({
|
|||||||
type === 'defaultSemiBold' ? styles.defaultSemiBold : undefined,
|
type === 'defaultSemiBold' ? styles.defaultSemiBold : undefined,
|
||||||
type === 'subtitle' ? styles.subtitle : undefined,
|
type === 'subtitle' ? styles.subtitle : undefined,
|
||||||
type === 'link' ? styles.link : undefined,
|
type === 'link' ? styles.link : undefined,
|
||||||
|
type === 'meta' ? styles.meta : undefined,
|
||||||
style,
|
style,
|
||||||
]}
|
]}
|
||||||
{...rest}
|
{...rest}
|
||||||
@ -57,4 +59,7 @@ const styles = StyleSheet.create({
|
|||||||
fontSize: 16,
|
fontSize: 16,
|
||||||
color: '#0a7ea4',
|
color: '#0a7ea4',
|
||||||
},
|
},
|
||||||
|
meta: {
|
||||||
|
fontSize: 14
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@ -18,6 +18,7 @@ export const Colors = {
|
|||||||
errorText: "#cf0404",
|
errorText: "#cf0404",
|
||||||
tint: tintColorLight,
|
tint: tintColorLight,
|
||||||
icon: '#687076',
|
icon: '#687076',
|
||||||
|
meta: '#919191',
|
||||||
tabIconDefault: '#687076',
|
tabIconDefault: '#687076',
|
||||||
tabIconSelected: tintColorLight,
|
tabIconSelected: tintColorLight,
|
||||||
},
|
},
|
||||||
@ -28,6 +29,7 @@ export const Colors = {
|
|||||||
errorText: "#cf0404",
|
errorText: "#cf0404",
|
||||||
tint: tintColorDark,
|
tint: tintColorDark,
|
||||||
icon: '#9BA1A6',
|
icon: '#9BA1A6',
|
||||||
|
meta: '#cfcfcf',
|
||||||
tabIconDefault: '#9BA1A6',
|
tabIconDefault: '#9BA1A6',
|
||||||
tabIconSelected: tintColorDark,
|
tabIconSelected: tintColorDark,
|
||||||
},
|
},
|
||||||
|
|||||||
6
utils/capitalize-first.ts
Normal file
6
utils/capitalize-first.ts
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
function capitalizeFirst(str: string) {
|
||||||
|
if (!str) return '';
|
||||||
|
return str.charAt(0).toUpperCase() + str.slice(1).toLowerCase();
|
||||||
|
}
|
||||||
|
|
||||||
|
export default capitalizeFirst
|
||||||
Loading…
x
Reference in New Issue
Block a user