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;
|
||||
}
|
||||
@ -1,6 +1,7 @@
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
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 = () => {
|
||||
return useQuery({
|
||||
@ -30,7 +31,7 @@ interface RandomUserTestAttrs {
|
||||
export const useRandomUserTestMutation = () => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation<any, AxiosError, RandomUserTestAttrs>({
|
||||
return useMutation<UserTestResponse, AxiosError, RandomUserTestAttrs>({
|
||||
mutationFn: async ({ min, max, category_id }: RandomUserTestAttrs) => {
|
||||
const response = await post_start_random_test(min, max, category_id);
|
||||
|
||||
@ -49,7 +50,7 @@ interface StartTestAttrs {
|
||||
export const useStartTestMutation = () => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation<any, AxiosError, StartTestAttrs>({
|
||||
return useMutation<UserTestResponse, AxiosError, StartTestAttrs>({
|
||||
mutationFn: async ({ test_id }: StartTestAttrs) => {
|
||||
const response = await post_start_test(test_id);
|
||||
|
||||
@ -60,4 +61,30 @@ export const useStartTestMutation = () => {
|
||||
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
|
||||
}, {
|
||||
onSuccess: (data) => {
|
||||
toast.show(`New user test created: ${data.id}` , { type: "success" })
|
||||
router.push(`/user-tests/doing/${data.id}`);
|
||||
},
|
||||
onError: (error) => {
|
||||
toast.show(getErrorAxiosMessage(error), { type: "danger" })
|
||||
|
||||
@ -6,7 +6,7 @@ import { ButtonText } from "@/components/ui/button";
|
||||
import Content from "@/components/ui/content";
|
||||
import CustomButton from "@/components/ui/custom-button";
|
||||
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 { useToast } from "react-native-toast-notifications";
|
||||
|
||||
@ -31,7 +31,9 @@ const TestScreen = () => {
|
||||
test_id: data.id,
|
||||
},
|
||||
{
|
||||
onSuccess: (data) => {},
|
||||
onSuccess: (data) => {
|
||||
router.push(`/user-tests/doing/${data.id}`);
|
||||
},
|
||||
onError: (error) => {
|
||||
toast.show(getErrorAxiosMessage(error), { type: "danger" });
|
||||
},
|
||||
|
||||
@ -8,7 +8,7 @@ import { Divider } from "@/components/ui/divider";
|
||||
import UserTest from "@/components/user-test";
|
||||
import { getUserTestStatus, TestStatus } from "@/utils/get-user-test-status";
|
||||
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";
|
||||
|
||||
const UserTestScreen = () => {
|
||||
@ -28,7 +28,7 @@ const UserTestScreen = () => {
|
||||
const displayCorrectAnswers = status !== TestStatus.InProgress;
|
||||
|
||||
const handleContinueTest = () => {
|
||||
|
||||
router.push(`/user-tests/doing/${userTest.id}`);
|
||||
}
|
||||
const isCorrectVariation = (answer: AnswerResponse, variant: QuestionVariant) => {
|
||||
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;
|
||||
isTextType?: boolean;
|
||||
|
||||
userAnswers?: string[] | number[];
|
||||
correctAnswers?: string[] | number[];
|
||||
userAnswers?: (string | number)[];
|
||||
correctAnswers?: (string | number)[];
|
||||
|
||||
isCorrectUserAnswer?: boolean;
|
||||
|
||||
@ -100,7 +100,7 @@ const Answer = ({
|
||||
if (!answer) return <Skeleton className="w-full h-16" />;
|
||||
|
||||
return (
|
||||
<Pressable onPress={onPress}>
|
||||
<Pressable onPress={(onPress)}>
|
||||
<Panel style={stylePanel} className="flex-row">
|
||||
<View style={styles.answerNumberation}>
|
||||
{isTextType ? (
|
||||
|
||||
@ -6,9 +6,9 @@ interface ContentProps extends ScrollViewProps {
|
||||
children?: React.ReactNode;
|
||||
}
|
||||
|
||||
const Content = forwardRef<ScrollView, ContentProps>(({ children, ...rest }, ref) => {
|
||||
const Content = forwardRef<ScrollView, ContentProps>(({ children, style, ...rest }, ref) => {
|
||||
return (
|
||||
<ScrollView ref={ref} style={styles.content} {...rest}>
|
||||
<ScrollView ref={ref} style={[styles.content, style]} {...rest}>
|
||||
{children}
|
||||
</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