Completed UserTEst doing
This commit is contained in:
parent
dad0063f44
commit
9568cb06b9
@ -123,5 +123,24 @@ export const post_start_test = async (test_id: number) => {
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
return response.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const post_test_complete = async (test_id: number) => {
|
||||||
|
const response = await axiosInstance.post<{ data: UserTestResponse }>(
|
||||||
|
`/api/user-tests/${test_id}/complete/`,
|
||||||
|
)
|
||||||
|
|
||||||
|
return response.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const post_test_answer = async (answerId: number, answer: (string | number)[]) => {
|
||||||
|
const response = await axiosInstance.post(
|
||||||
|
`/api/user-test-answers/${answerId}/submit/`,
|
||||||
|
{
|
||||||
|
answer
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
return response.data;
|
return response.data;
|
||||||
}
|
}
|
||||||
@ -1,6 +1,7 @@
|
|||||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||||
import { AxiosError } from "axios";
|
import { AxiosError } from "axios";
|
||||||
import { get_my_user_tests, get_user_test, post_start_random_test, post_start_test } from "./_client";
|
import { get_my_user_tests, get_user_test, post_start_random_test, post_start_test, post_test_answer, post_test_complete } from "./_client";
|
||||||
|
import { UserTestResponse } from "./types";
|
||||||
|
|
||||||
export const useMyTests = () => {
|
export const useMyTests = () => {
|
||||||
return useQuery({
|
return useQuery({
|
||||||
@ -30,7 +31,7 @@ interface RandomUserTestAttrs {
|
|||||||
export const useRandomUserTestMutation = () => {
|
export const useRandomUserTestMutation = () => {
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
return useMutation<any, AxiosError, RandomUserTestAttrs>({
|
return useMutation<UserTestResponse, AxiosError, RandomUserTestAttrs>({
|
||||||
mutationFn: async ({ min, max, category_id }: RandomUserTestAttrs) => {
|
mutationFn: async ({ min, max, category_id }: RandomUserTestAttrs) => {
|
||||||
const response = await post_start_random_test(min, max, category_id);
|
const response = await post_start_random_test(min, max, category_id);
|
||||||
|
|
||||||
@ -49,7 +50,7 @@ interface StartTestAttrs {
|
|||||||
export const useStartTestMutation = () => {
|
export const useStartTestMutation = () => {
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
return useMutation<any, AxiosError, StartTestAttrs>({
|
return useMutation<UserTestResponse, AxiosError, StartTestAttrs>({
|
||||||
mutationFn: async ({ test_id }: StartTestAttrs) => {
|
mutationFn: async ({ test_id }: StartTestAttrs) => {
|
||||||
const response = await post_start_test(test_id);
|
const response = await post_start_test(test_id);
|
||||||
|
|
||||||
@ -60,4 +61,30 @@ export const useStartTestMutation = () => {
|
|||||||
return response.data;
|
return response.data;
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
interface AnswerAttrs {
|
||||||
|
answerId: number;
|
||||||
|
answer: (string | number)[];
|
||||||
|
}
|
||||||
|
export const useAnswerMutation = () => {
|
||||||
|
return useMutation<any, AxiosError, AnswerAttrs>({
|
||||||
|
mutationFn: ({ answerId, answer }) => post_test_answer(answerId, answer)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
interface CompleteTestAttrs {
|
||||||
|
testId: number;
|
||||||
|
}
|
||||||
|
export const useCompleteTestMutation = () => {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
|
return useMutation<any, AxiosError, CompleteTestAttrs>({
|
||||||
|
mutationFn: ({testId}) => post_test_complete(testId),
|
||||||
|
onSuccess: (_, { testId }) => {
|
||||||
|
queryClient.invalidateQueries({
|
||||||
|
queryKey: ["user-test", testId]
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
@ -34,7 +34,7 @@ export default function HomeScreen() {
|
|||||||
category_id: catId
|
category_id: catId
|
||||||
}, {
|
}, {
|
||||||
onSuccess: (data) => {
|
onSuccess: (data) => {
|
||||||
toast.show(`New user test created: ${data.id}` , { type: "success" })
|
router.push(`/user-tests/doing/${data.id}`);
|
||||||
},
|
},
|
||||||
onError: (error) => {
|
onError: (error) => {
|
||||||
toast.show(getErrorAxiosMessage(error), { type: "danger" })
|
toast.show(getErrorAxiosMessage(error), { type: "danger" })
|
||||||
|
|||||||
@ -6,7 +6,7 @@ import { ButtonText } from "@/components/ui/button";
|
|||||||
import Content from "@/components/ui/content";
|
import Content from "@/components/ui/content";
|
||||||
import CustomButton from "@/components/ui/custom-button";
|
import CustomButton from "@/components/ui/custom-button";
|
||||||
import getErrorAxiosMessage from "@/utils/get-error-axios-message";
|
import getErrorAxiosMessage from "@/utils/get-error-axios-message";
|
||||||
import { Stack, useLocalSearchParams } from "expo-router";
|
import { router, Stack, useLocalSearchParams } from "expo-router";
|
||||||
import { View } from "react-native";
|
import { View } from "react-native";
|
||||||
import { useToast } from "react-native-toast-notifications";
|
import { useToast } from "react-native-toast-notifications";
|
||||||
|
|
||||||
@ -31,7 +31,9 @@ const TestScreen = () => {
|
|||||||
test_id: data.id,
|
test_id: data.id,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
onSuccess: (data) => {},
|
onSuccess: (data) => {
|
||||||
|
router.push(`/user-tests/doing/${data.id}`);
|
||||||
|
},
|
||||||
onError: (error) => {
|
onError: (error) => {
|
||||||
toast.show(getErrorAxiosMessage(error), { type: "danger" });
|
toast.show(getErrorAxiosMessage(error), { type: "danger" });
|
||||||
},
|
},
|
||||||
|
|||||||
@ -8,7 +8,7 @@ import { Divider } from "@/components/ui/divider";
|
|||||||
import UserTest from "@/components/user-test";
|
import UserTest from "@/components/user-test";
|
||||||
import { getUserTestStatus, TestStatus } from "@/utils/get-user-test-status";
|
import { getUserTestStatus, TestStatus } from "@/utils/get-user-test-status";
|
||||||
import getUserTestTitle from "@/utils/get-user-test-title";
|
import getUserTestTitle from "@/utils/get-user-test-title";
|
||||||
import { Stack, useLocalSearchParams } from "expo-router";
|
import { router, Stack, useLocalSearchParams } from "expo-router";
|
||||||
import { View } from "react-native";
|
import { View } from "react-native";
|
||||||
|
|
||||||
const UserTestScreen = () => {
|
const UserTestScreen = () => {
|
||||||
@ -28,7 +28,7 @@ const UserTestScreen = () => {
|
|||||||
const displayCorrectAnswers = status !== TestStatus.InProgress;
|
const displayCorrectAnswers = status !== TestStatus.InProgress;
|
||||||
|
|
||||||
const handleContinueTest = () => {
|
const handleContinueTest = () => {
|
||||||
|
router.push(`/user-tests/doing/${userTest.id}`);
|
||||||
}
|
}
|
||||||
const isCorrectVariation = (answer: AnswerResponse, variant: QuestionVariant) => {
|
const isCorrectVariation = (answer: AnswerResponse, variant: QuestionVariant) => {
|
||||||
if(answer.is_correct) return true;
|
if(answer.is_correct) return true;
|
||||||
|
|||||||
272
app/user-tests/doing/[id].tsx
Normal file
272
app/user-tests/doing/[id].tsx
Normal file
@ -0,0 +1,272 @@
|
|||||||
|
import { QuestionTypes } from "@/api/types";
|
||||||
|
import {
|
||||||
|
useAnswerMutation,
|
||||||
|
useCompleteTestMutation,
|
||||||
|
useUserTest,
|
||||||
|
} from "@/api/userTests";
|
||||||
|
import Question from "@/components/question";
|
||||||
|
import {
|
||||||
|
AlertDialog,
|
||||||
|
AlertDialogBackdrop,
|
||||||
|
AlertDialogBody,
|
||||||
|
AlertDialogContent,
|
||||||
|
AlertDialogFooter,
|
||||||
|
AlertDialogHeader,
|
||||||
|
} from "@/components/ui/alert-dialog";
|
||||||
|
import Answer from "@/components/ui/answer";
|
||||||
|
import { Button, ButtonText } from "@/components/ui/button";
|
||||||
|
import Content from "@/components/ui/content";
|
||||||
|
import CustomButton from "@/components/ui/custom-button";
|
||||||
|
import { Divider } from "@/components/ui/divider";
|
||||||
|
import Panel from "@/components/ui/panel";
|
||||||
|
import { Textarea, TextareaInput } from "@/components/ui/textarea";
|
||||||
|
import { ThemedText } from "@/components/ui/themed-text";
|
||||||
|
import getErrorAxiosMessage from "@/utils/get-error-axios-message";
|
||||||
|
import getUserTestTitle from "@/utils/get-user-test-title";
|
||||||
|
import { router, Stack, useLocalSearchParams } from "expo-router";
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { ScrollView, StyleSheet, View } from "react-native";
|
||||||
|
import { useToast } from "react-native-toast-notifications";
|
||||||
|
|
||||||
|
const DoingUserTestScreen = () => {
|
||||||
|
const { id: idParam } = useLocalSearchParams<{ id: string }>();
|
||||||
|
const [answer, setAnswer] = useState<(string | number)[]>([]);
|
||||||
|
const [page, setPage] = useState(0);
|
||||||
|
const [openCompleteDialog, setOpenCompleteDialog] = useState(false);
|
||||||
|
const id = +idParam;
|
||||||
|
const { data: userTest, refetch: refetchUserTest } = useUserTest(id);
|
||||||
|
const { mutateAsync: mutateAsyncAnswer, isPending: answerPending } =
|
||||||
|
useAnswerMutation();
|
||||||
|
const { mutate: mutateCompleteTest, isPending: completePending } = useCompleteTestMutation();
|
||||||
|
const toast = useToast();
|
||||||
|
useEffect(() => {
|
||||||
|
if (!userTest) return;
|
||||||
|
|
||||||
|
setAnswer(userTest.answers[page].answer || []);
|
||||||
|
}, [userTest, page]);
|
||||||
|
|
||||||
|
if (!userTest)
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Stack.Screen options={{ title: `Test #${id}` }} />
|
||||||
|
<Content>
|
||||||
|
<Question />
|
||||||
|
</Content>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
|
||||||
|
const currentAnswer = userTest.answers[page];
|
||||||
|
const currentQuestion = currentAnswer.question;
|
||||||
|
const pages = Array.from({ length: userTest.answers.length }).map(
|
||||||
|
(_, i) => i
|
||||||
|
);
|
||||||
|
const countAnsweredQuestion = userTest.answers.filter(
|
||||||
|
(answer) => answer.answer
|
||||||
|
).length;
|
||||||
|
const isAllAnswered = countAnsweredQuestion === userTest.answers.length;
|
||||||
|
const isLastPage = page === pages.length - 1;
|
||||||
|
const userTextAnswer = answer ? (answer[0] as string) : "";
|
||||||
|
const alertDialogMessage = isAllAnswered
|
||||||
|
? "Finish the test to see your results."
|
||||||
|
: "Warning: You haven't answered all questions. Are you sure you want to finish?";
|
||||||
|
|
||||||
|
const handleSetAnswer = (value: number | string) => {
|
||||||
|
if (currentQuestion.type === QuestionTypes.Text) {
|
||||||
|
setAnswer([value]);
|
||||||
|
}
|
||||||
|
if (currentQuestion.type === QuestionTypes.Single) {
|
||||||
|
setAnswer([value as number]);
|
||||||
|
}
|
||||||
|
if (currentQuestion.type === QuestionTypes.Multiple) {
|
||||||
|
if ((answer as number[]).includes(value as number)) {
|
||||||
|
setAnswer((prev) => prev.filter((v) => v !== value));
|
||||||
|
} else {
|
||||||
|
setAnswer((prev) => {
|
||||||
|
const set = new Set(prev as number[]);
|
||||||
|
set.add(value as number);
|
||||||
|
return Array.from(set);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const handleSaveAnswer = async () => {
|
||||||
|
let ans = answer;
|
||||||
|
|
||||||
|
if (currentQuestion.type === QuestionTypes.Text && answer) {
|
||||||
|
ans = [(answer[0] as string).trim()];
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await mutateAsyncAnswer(
|
||||||
|
{
|
||||||
|
answerId: currentAnswer.id,
|
||||||
|
answer: ans,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
onSuccess: () => {
|
||||||
|
toast.show("Your answer was saved", { type: "success" });
|
||||||
|
if(!isLastPage)
|
||||||
|
goPage(page + 1);
|
||||||
|
},
|
||||||
|
onError: (error) => {
|
||||||
|
toast.show(getErrorAxiosMessage(error), { type: "danger" });
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
} catch {}
|
||||||
|
await refetchUserTest();
|
||||||
|
};
|
||||||
|
const handleCompleteTest = () => {
|
||||||
|
mutateCompleteTest({
|
||||||
|
testId: userTest.id
|
||||||
|
}, {
|
||||||
|
onSuccess: () => {
|
||||||
|
router.replace(`/user-tests/${userTest.id}`);
|
||||||
|
},
|
||||||
|
onError: (error) => {
|
||||||
|
toast.show(getErrorAxiosMessage(error), { type: "danger" });
|
||||||
|
}
|
||||||
|
})
|
||||||
|
};
|
||||||
|
const goPage = (page: number) => {
|
||||||
|
setAnswer([]);
|
||||||
|
setPage(page);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Stack.Screen options={{ title: getUserTestTitle(userTest) }} />
|
||||||
|
<View style={styles.container}>
|
||||||
|
<Content style={styles.content}>
|
||||||
|
<View style={styles.top}>
|
||||||
|
<ScrollView horizontal contentContainerStyle={styles.topScrollView}>
|
||||||
|
{pages.map((p) => (
|
||||||
|
<Button
|
||||||
|
key={p}
|
||||||
|
variant={p === page ? "solid" : "outline"}
|
||||||
|
action={
|
||||||
|
p === page || userTest.answers[p].answer
|
||||||
|
? "primary"
|
||||||
|
: "secondary"
|
||||||
|
}
|
||||||
|
onPress={() => goPage(p)}
|
||||||
|
>
|
||||||
|
<ButtonText>{p + 1}</ButtonText>
|
||||||
|
</Button>
|
||||||
|
))}
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
action="negative"
|
||||||
|
onPress={() => setOpenCompleteDialog(true)}
|
||||||
|
>
|
||||||
|
<ButtonText>Complete test</ButtonText>
|
||||||
|
</Button>
|
||||||
|
</ScrollView>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<Question question={currentQuestion} />
|
||||||
|
|
||||||
|
<Divider className="mt-5 mb-5" />
|
||||||
|
|
||||||
|
<View className="gap-4 mb-4">
|
||||||
|
{currentQuestion.variants.map((variant) => (
|
||||||
|
<Answer
|
||||||
|
key={variant.id}
|
||||||
|
answer={variant}
|
||||||
|
isTextType={false}
|
||||||
|
userAnswers={answer}
|
||||||
|
onPress={() => handleSetAnswer(variant.id)}
|
||||||
|
displayUserAnswer={true}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
{currentQuestion.type === QuestionTypes.Text && (
|
||||||
|
<Panel>
|
||||||
|
<ThemedText className="mb-1 text-sm" type="defaultSemiBold">
|
||||||
|
Your answer:
|
||||||
|
</ThemedText>
|
||||||
|
<Textarea>
|
||||||
|
<TextareaInput
|
||||||
|
onChangeText={(text) => handleSetAnswer(text)}
|
||||||
|
value={userTextAnswer}
|
||||||
|
/>
|
||||||
|
</Textarea>
|
||||||
|
</Panel>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<CustomButton isLoading={answerPending} onPress={handleSaveAnswer}>
|
||||||
|
<ButtonText>Save answer</ButtonText>
|
||||||
|
</CustomButton>
|
||||||
|
|
||||||
|
<Divider className="mt-2 mb-3" />
|
||||||
|
|
||||||
|
{(isAllAnswered || isLastPage) && (
|
||||||
|
<Button
|
||||||
|
variant={isAllAnswered ? "outline" : "solid"}
|
||||||
|
action="negative"
|
||||||
|
onPress={() => setOpenCompleteDialog(true)}
|
||||||
|
>
|
||||||
|
<ButtonText>Complete test</ButtonText>
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</Content>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<AlertDialog
|
||||||
|
isOpen={openCompleteDialog}
|
||||||
|
onClose={() => setOpenCompleteDialog(false)}
|
||||||
|
>
|
||||||
|
<AlertDialogBackdrop />
|
||||||
|
<AlertDialogContent>
|
||||||
|
<AlertDialogHeader className="mb-2">
|
||||||
|
<ThemedText type="defaultSemiBold">Complete test</ThemedText>
|
||||||
|
</AlertDialogHeader>
|
||||||
|
<AlertDialogBody className="mb-3">
|
||||||
|
<ThemedText>{alertDialogMessage}</ThemedText>
|
||||||
|
</AlertDialogBody>
|
||||||
|
<AlertDialogFooter>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
action="secondary"
|
||||||
|
onPress={() => setOpenCompleteDialog(false)}
|
||||||
|
size="sm"
|
||||||
|
>
|
||||||
|
<ButtonText>Cancel</ButtonText>
|
||||||
|
</Button>
|
||||||
|
<CustomButton
|
||||||
|
variant={isAllAnswered ? "outline" : "solid"}
|
||||||
|
action="negative"
|
||||||
|
size="sm"
|
||||||
|
isLoading={completePending}
|
||||||
|
onPress={handleCompleteTest}
|
||||||
|
>
|
||||||
|
<ButtonText>Complete</ButtonText>
|
||||||
|
</CustomButton>
|
||||||
|
</AlertDialogFooter>
|
||||||
|
</AlertDialogContent>
|
||||||
|
</AlertDialog>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const styles = StyleSheet.create({
|
||||||
|
container: {
|
||||||
|
flex: 1,
|
||||||
|
alignItems: "flex-start",
|
||||||
|
},
|
||||||
|
top: {
|
||||||
|
width: "100%",
|
||||||
|
marginBottom: 15,
|
||||||
|
},
|
||||||
|
topScrollView: {
|
||||||
|
flexDirection: "row",
|
||||||
|
alignItems: "flex-start",
|
||||||
|
gap: 10,
|
||||||
|
},
|
||||||
|
content: {
|
||||||
|
flex: 1,
|
||||||
|
width: "100%",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export default DoingUserTestScreen;
|
||||||
296
components/ui/alert-dialog/index.tsx
Normal file
296
components/ui/alert-dialog/index.tsx
Normal file
@ -0,0 +1,296 @@
|
|||||||
|
'use client';
|
||||||
|
import React from 'react';
|
||||||
|
import { createAlertDialog } from '@gluestack-ui/core/alert-dialog/creator';
|
||||||
|
import { tva } from '@gluestack-ui/utils/nativewind-utils';
|
||||||
|
import {
|
||||||
|
withStyleContext,
|
||||||
|
useStyleContext,
|
||||||
|
} from '@gluestack-ui/utils/nativewind-utils';
|
||||||
|
|
||||||
|
import { cssInterop } from 'nativewind';
|
||||||
|
import type { VariantProps } from '@gluestack-ui/utils/nativewind-utils';
|
||||||
|
import {
|
||||||
|
Motion,
|
||||||
|
AnimatePresence,
|
||||||
|
createMotionAnimatedComponent,
|
||||||
|
MotionComponentProps,
|
||||||
|
} from '@legendapp/motion';
|
||||||
|
import { View, Pressable, ScrollView, ViewStyle } from 'react-native';
|
||||||
|
|
||||||
|
const SCOPE = 'ALERT_DIALOG';
|
||||||
|
|
||||||
|
const RootComponent = withStyleContext(View, SCOPE);
|
||||||
|
|
||||||
|
type IMotionViewProps = React.ComponentProps<typeof View> &
|
||||||
|
MotionComponentProps<typeof View, ViewStyle, unknown, unknown, unknown>;
|
||||||
|
|
||||||
|
const MotionView = Motion.View as React.ComponentType<IMotionViewProps>;
|
||||||
|
|
||||||
|
type IAnimatedPressableProps = React.ComponentProps<typeof Pressable> &
|
||||||
|
MotionComponentProps<typeof Pressable, ViewStyle, unknown, unknown, unknown>;
|
||||||
|
|
||||||
|
const AnimatedPressable = createMotionAnimatedComponent(
|
||||||
|
Pressable
|
||||||
|
) as React.ComponentType<IAnimatedPressableProps>;
|
||||||
|
|
||||||
|
const UIAccessibleAlertDialog = createAlertDialog({
|
||||||
|
Root: RootComponent,
|
||||||
|
Body: ScrollView,
|
||||||
|
Content: MotionView,
|
||||||
|
CloseButton: Pressable,
|
||||||
|
Header: View,
|
||||||
|
Footer: View,
|
||||||
|
Backdrop: AnimatedPressable,
|
||||||
|
AnimatePresence: AnimatePresence,
|
||||||
|
});
|
||||||
|
|
||||||
|
cssInterop(MotionView, { className: 'style' });
|
||||||
|
cssInterop(AnimatedPressable, { className: 'style' });
|
||||||
|
|
||||||
|
const alertDialogStyle = tva({
|
||||||
|
base: 'group/modal w-full h-full justify-center items-center web:pointer-events-none',
|
||||||
|
parentVariants: {
|
||||||
|
size: {
|
||||||
|
xs: '',
|
||||||
|
sm: '',
|
||||||
|
md: '',
|
||||||
|
lg: '',
|
||||||
|
full: '',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const alertDialogContentStyle = tva({
|
||||||
|
base: 'bg-background-0 rounded-lg overflow-hidden border border-outline-100 p-6',
|
||||||
|
parentVariants: {
|
||||||
|
size: {
|
||||||
|
xs: 'w-[60%] max-w-[360px]',
|
||||||
|
sm: 'w-[70%] max-w-[420px]',
|
||||||
|
md: 'w-[80%] max-w-[510px]',
|
||||||
|
lg: 'w-[90%] max-w-[640px]',
|
||||||
|
full: 'w-full',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const alertDialogCloseButtonStyle = tva({
|
||||||
|
base: 'group/alert-dialog-close-button z-10 rounded-sm p-2 data-[focus-visible=true]:bg-background-100 web:cursor-pointer outline-0',
|
||||||
|
});
|
||||||
|
|
||||||
|
const alertDialogHeaderStyle = tva({
|
||||||
|
base: 'justify-between items-center flex-row',
|
||||||
|
});
|
||||||
|
|
||||||
|
const alertDialogFooterStyle = tva({
|
||||||
|
base: 'flex-row justify-end items-center gap-3',
|
||||||
|
});
|
||||||
|
|
||||||
|
const alertDialogBodyStyle = tva({ base: '' });
|
||||||
|
|
||||||
|
const alertDialogBackdropStyle = tva({
|
||||||
|
base: 'absolute left-0 top-0 right-0 bottom-0 bg-background-dark web:cursor-default',
|
||||||
|
});
|
||||||
|
|
||||||
|
type IAlertDialogProps = React.ComponentPropsWithoutRef<
|
||||||
|
typeof UIAccessibleAlertDialog
|
||||||
|
> &
|
||||||
|
VariantProps<typeof alertDialogStyle>;
|
||||||
|
|
||||||
|
type IAlertDialogContentProps = React.ComponentPropsWithoutRef<
|
||||||
|
typeof UIAccessibleAlertDialog.Content
|
||||||
|
> &
|
||||||
|
VariantProps<typeof alertDialogContentStyle> & { className?: string };
|
||||||
|
|
||||||
|
type IAlertDialogCloseButtonProps = React.ComponentPropsWithoutRef<
|
||||||
|
typeof UIAccessibleAlertDialog.CloseButton
|
||||||
|
> &
|
||||||
|
VariantProps<typeof alertDialogCloseButtonStyle>;
|
||||||
|
|
||||||
|
type IAlertDialogHeaderProps = React.ComponentPropsWithoutRef<
|
||||||
|
typeof UIAccessibleAlertDialog.Header
|
||||||
|
> &
|
||||||
|
VariantProps<typeof alertDialogHeaderStyle>;
|
||||||
|
|
||||||
|
type IAlertDialogFooterProps = React.ComponentPropsWithoutRef<
|
||||||
|
typeof UIAccessibleAlertDialog.Footer
|
||||||
|
> &
|
||||||
|
VariantProps<typeof alertDialogFooterStyle>;
|
||||||
|
|
||||||
|
type IAlertDialogBodyProps = React.ComponentPropsWithoutRef<
|
||||||
|
typeof UIAccessibleAlertDialog.Body
|
||||||
|
> &
|
||||||
|
VariantProps<typeof alertDialogBodyStyle>;
|
||||||
|
|
||||||
|
type IAlertDialogBackdropProps = React.ComponentPropsWithoutRef<
|
||||||
|
typeof UIAccessibleAlertDialog.Backdrop
|
||||||
|
> &
|
||||||
|
VariantProps<typeof alertDialogBackdropStyle> & { className?: string };
|
||||||
|
|
||||||
|
const AlertDialog = React.forwardRef<
|
||||||
|
React.ComponentRef<typeof UIAccessibleAlertDialog>,
|
||||||
|
IAlertDialogProps
|
||||||
|
>(function AlertDialog({ className, size = 'md', ...props }, ref) {
|
||||||
|
return (
|
||||||
|
<UIAccessibleAlertDialog
|
||||||
|
ref={ref}
|
||||||
|
{...props}
|
||||||
|
className={alertDialogStyle({ class: className })}
|
||||||
|
context={{ size }}
|
||||||
|
pointerEvents="box-none"
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
const AlertDialogContent = React.forwardRef<
|
||||||
|
React.ComponentRef<typeof UIAccessibleAlertDialog.Content>,
|
||||||
|
IAlertDialogContentProps
|
||||||
|
>(function AlertDialogContent({ className, size, ...props }, ref) {
|
||||||
|
const { size: parentSize } = useStyleContext(SCOPE);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<UIAccessibleAlertDialog.Content
|
||||||
|
pointerEvents="auto"
|
||||||
|
ref={ref}
|
||||||
|
initial={{
|
||||||
|
scale: 0.9,
|
||||||
|
opacity: 0,
|
||||||
|
}}
|
||||||
|
animate={{
|
||||||
|
scale: 1,
|
||||||
|
opacity: 1,
|
||||||
|
}}
|
||||||
|
exit={{
|
||||||
|
scale: 0.9,
|
||||||
|
opacity: 0,
|
||||||
|
}}
|
||||||
|
transition={{
|
||||||
|
type: 'spring',
|
||||||
|
damping: 18,
|
||||||
|
stiffness: 250,
|
||||||
|
opacity: {
|
||||||
|
type: 'timing',
|
||||||
|
duration: 250,
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
{...props}
|
||||||
|
className={alertDialogContentStyle({
|
||||||
|
parentVariants: {
|
||||||
|
size: parentSize,
|
||||||
|
},
|
||||||
|
size,
|
||||||
|
class: className,
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
const AlertDialogCloseButton = React.forwardRef<
|
||||||
|
React.ComponentRef<typeof UIAccessibleAlertDialog.CloseButton>,
|
||||||
|
IAlertDialogCloseButtonProps
|
||||||
|
>(function AlertDialogCloseButton({ className, ...props }, ref) {
|
||||||
|
return (
|
||||||
|
<UIAccessibleAlertDialog.CloseButton
|
||||||
|
ref={ref}
|
||||||
|
{...props}
|
||||||
|
className={alertDialogCloseButtonStyle({
|
||||||
|
class: className,
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
const AlertDialogHeader = React.forwardRef<
|
||||||
|
React.ComponentRef<typeof UIAccessibleAlertDialog.Header>,
|
||||||
|
IAlertDialogHeaderProps
|
||||||
|
>(function AlertDialogHeader({ className, ...props }, ref) {
|
||||||
|
return (
|
||||||
|
<UIAccessibleAlertDialog.Header
|
||||||
|
ref={ref}
|
||||||
|
{...props}
|
||||||
|
className={alertDialogHeaderStyle({
|
||||||
|
class: className,
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
const AlertDialogFooter = React.forwardRef<
|
||||||
|
React.ComponentRef<typeof UIAccessibleAlertDialog.Footer>,
|
||||||
|
IAlertDialogFooterProps
|
||||||
|
>(function AlertDialogFooter({ className, ...props }, ref) {
|
||||||
|
return (
|
||||||
|
<UIAccessibleAlertDialog.Footer
|
||||||
|
ref={ref}
|
||||||
|
{...props}
|
||||||
|
className={alertDialogFooterStyle({
|
||||||
|
class: className,
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
const AlertDialogBody = React.forwardRef<
|
||||||
|
React.ComponentRef<typeof UIAccessibleAlertDialog.Body>,
|
||||||
|
IAlertDialogBodyProps
|
||||||
|
>(function AlertDialogBody({ className, ...props }, ref) {
|
||||||
|
return (
|
||||||
|
<UIAccessibleAlertDialog.Body
|
||||||
|
ref={ref}
|
||||||
|
{...props}
|
||||||
|
className={alertDialogBodyStyle({
|
||||||
|
class: className,
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
const AlertDialogBackdrop = React.forwardRef<
|
||||||
|
React.ComponentRef<typeof UIAccessibleAlertDialog.Backdrop>,
|
||||||
|
IAlertDialogBackdropProps
|
||||||
|
>(function AlertDialogBackdrop({ className, ...props }, ref) {
|
||||||
|
return (
|
||||||
|
<UIAccessibleAlertDialog.Backdrop
|
||||||
|
ref={ref}
|
||||||
|
initial={{
|
||||||
|
opacity: 0,
|
||||||
|
}}
|
||||||
|
animate={{
|
||||||
|
opacity: 0.5,
|
||||||
|
}}
|
||||||
|
exit={{
|
||||||
|
opacity: 0,
|
||||||
|
}}
|
||||||
|
transition={{
|
||||||
|
type: 'spring',
|
||||||
|
damping: 18,
|
||||||
|
stiffness: 250,
|
||||||
|
opacity: {
|
||||||
|
type: 'timing',
|
||||||
|
duration: 250,
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
{...props}
|
||||||
|
className={alertDialogBackdropStyle({
|
||||||
|
class: className,
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
AlertDialog.displayName = 'AlertDialog';
|
||||||
|
AlertDialogContent.displayName = 'AlertDialogContent';
|
||||||
|
AlertDialogCloseButton.displayName = 'AlertDialogCloseButton';
|
||||||
|
AlertDialogHeader.displayName = 'AlertDialogHeader';
|
||||||
|
AlertDialogFooter.displayName = 'AlertDialogFooter';
|
||||||
|
AlertDialogBody.displayName = 'AlertDialogBody';
|
||||||
|
AlertDialogBackdrop.displayName = 'AlertDialogBackdrop';
|
||||||
|
|
||||||
|
export {
|
||||||
|
AlertDialog,
|
||||||
|
AlertDialogContent,
|
||||||
|
AlertDialogCloseButton,
|
||||||
|
AlertDialogHeader,
|
||||||
|
AlertDialogFooter,
|
||||||
|
AlertDialogBody,
|
||||||
|
AlertDialogBackdrop,
|
||||||
|
};
|
||||||
@ -10,8 +10,8 @@ interface AnswerProps {
|
|||||||
answer?: QuestionVariant;
|
answer?: QuestionVariant;
|
||||||
isTextType?: boolean;
|
isTextType?: boolean;
|
||||||
|
|
||||||
userAnswers?: string[] | number[];
|
userAnswers?: (string | number)[];
|
||||||
correctAnswers?: string[] | number[];
|
correctAnswers?: (string | number)[];
|
||||||
|
|
||||||
isCorrectUserAnswer?: boolean;
|
isCorrectUserAnswer?: boolean;
|
||||||
|
|
||||||
@ -100,7 +100,7 @@ const Answer = ({
|
|||||||
if (!answer) return <Skeleton className="w-full h-16" />;
|
if (!answer) return <Skeleton className="w-full h-16" />;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Pressable onPress={onPress}>
|
<Pressable onPress={(onPress)}>
|
||||||
<Panel style={stylePanel} className="flex-row">
|
<Panel style={stylePanel} className="flex-row">
|
||||||
<View style={styles.answerNumberation}>
|
<View style={styles.answerNumberation}>
|
||||||
{isTextType ? (
|
{isTextType ? (
|
||||||
|
|||||||
@ -6,9 +6,9 @@ interface ContentProps extends ScrollViewProps {
|
|||||||
children?: React.ReactNode;
|
children?: React.ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
const Content = forwardRef<ScrollView, ContentProps>(({ children, ...rest }, ref) => {
|
const Content = forwardRef<ScrollView, ContentProps>(({ children, style, ...rest }, ref) => {
|
||||||
return (
|
return (
|
||||||
<ScrollView ref={ref} style={styles.content} {...rest}>
|
<ScrollView ref={ref} style={[styles.content, style]} {...rest}>
|
||||||
{children}
|
{children}
|
||||||
</ScrollView>
|
</ScrollView>
|
||||||
);
|
);
|
||||||
|
|||||||
94
components/ui/textarea/index.tsx
Normal file
94
components/ui/textarea/index.tsx
Normal file
@ -0,0 +1,94 @@
|
|||||||
|
'use client';
|
||||||
|
import React from 'react';
|
||||||
|
import { createTextarea } from '@gluestack-ui/core/textarea/creator';
|
||||||
|
import { View, TextInput } from 'react-native';
|
||||||
|
import { tva } from '@gluestack-ui/utils/nativewind-utils';
|
||||||
|
import {
|
||||||
|
withStyleContext,
|
||||||
|
useStyleContext,
|
||||||
|
} from '@gluestack-ui/utils/nativewind-utils';
|
||||||
|
import type { VariantProps } from '@gluestack-ui/utils/nativewind-utils';
|
||||||
|
|
||||||
|
const SCOPE = 'TEXTAREA';
|
||||||
|
const UITextarea = createTextarea({
|
||||||
|
Root: withStyleContext(View, SCOPE),
|
||||||
|
Input: TextInput,
|
||||||
|
});
|
||||||
|
|
||||||
|
const textareaStyle = tva({
|
||||||
|
base: 'w-full h-[100px] border border-background-300 rounded data-[hover=true]:border-outline-400 data-[focus=true]:border-primary-700 data-[focus=true]:data-[hover=true]:border-primary-700 data-[disabled=true]:opacity-40 data-[disabled=true]:bg-background-50 data-[disabled=true]:data-[hover=true]:border-background-300',
|
||||||
|
|
||||||
|
variants: {
|
||||||
|
variant: {
|
||||||
|
default:
|
||||||
|
'data-[focus=true]:border-primary-700 data-[focus=true]:web:ring-1 data-[focus=true]:web:ring-inset data-[focus=true]:web:ring-indicator-primary data-[invalid=true]:border-error-700 data-[invalid=true]:web:ring-1 data-[invalid=true]:web:ring-inset data-[invalid=true]:web:ring-indicator-error data-[invalid=true]:data-[hover=true]:border-error-700 data-[invalid=true]:data-[focus=true]:data-[hover=true]:border-primary-700 data-[invalid=true]:data-[focus=true]:data-[hover=true]:web:ring-1 data-[invalid=true]:data-[focus=true]:data-[hover=true]:web:ring-inset data-[invalid=true]:data-[focus=true]:data-[hover=true]:web:ring-indicator-primary data-[invalid=true]:data-[disabled=true]:data-[hover=true]:border-error-700 data-[invalid=true]:data-[disabled=true]:data-[hover=true]:web:ring-1 data-[invalid=true]:data-[disabled=true]:data-[hover=true]:web:ring-inset data-[invalid=true]:data-[disabled=true]:data-[hover=true]:web:ring-indicator-error ',
|
||||||
|
},
|
||||||
|
size: {
|
||||||
|
sm: '',
|
||||||
|
md: '',
|
||||||
|
lg: '',
|
||||||
|
xl: '',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const textareaInputStyle = tva({
|
||||||
|
base: 'p-2 web:outline-0 web:outline-none flex-1 color-typography-900 placeholder:text-typography-500 web:cursor-text web:data-[disabled=true]:cursor-not-allowed',
|
||||||
|
parentVariants: {
|
||||||
|
size: {
|
||||||
|
sm: 'text-sm',
|
||||||
|
md: 'text-base',
|
||||||
|
lg: 'text-lg',
|
||||||
|
xl: 'text-xl',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
type ITextareaProps = React.ComponentProps<typeof UITextarea> &
|
||||||
|
VariantProps<typeof textareaStyle>;
|
||||||
|
|
||||||
|
const Textarea = React.forwardRef<
|
||||||
|
React.ComponentRef<typeof UITextarea>,
|
||||||
|
ITextareaProps
|
||||||
|
>(function Textarea(
|
||||||
|
{ className, variant = 'default', size = 'md', ...props },
|
||||||
|
ref
|
||||||
|
) {
|
||||||
|
return (
|
||||||
|
<UITextarea
|
||||||
|
ref={ref}
|
||||||
|
{...props}
|
||||||
|
className={textareaStyle({ variant, class: className })}
|
||||||
|
context={{ size }}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
type ITextareaInputProps = React.ComponentProps<typeof UITextarea.Input> &
|
||||||
|
VariantProps<typeof textareaInputStyle>;
|
||||||
|
|
||||||
|
const TextareaInput = React.forwardRef<
|
||||||
|
React.ComponentRef<typeof UITextarea.Input>,
|
||||||
|
ITextareaInputProps
|
||||||
|
>(function TextareaInput({ className, ...props }, ref) {
|
||||||
|
const { size: parentSize } = useStyleContext(SCOPE);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<UITextarea.Input
|
||||||
|
ref={ref}
|
||||||
|
{...props}
|
||||||
|
textAlignVertical="top"
|
||||||
|
className={textareaInputStyle({
|
||||||
|
parentVariants: {
|
||||||
|
size: parentSize,
|
||||||
|
},
|
||||||
|
class: className,
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
Textarea.displayName = 'Textarea';
|
||||||
|
TextareaInput.displayName = 'TextareaInput';
|
||||||
|
|
||||||
|
export { Textarea, TextareaInput };
|
||||||
Loading…
x
Reference in New Issue
Block a user