Completed UserTEst doing

This commit is contained in:
Stepan 2026-01-06 20:18:23 +01:00
parent dad0063f44
commit 9568cb06b9
10 changed files with 723 additions and 13 deletions

View File

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

View File

@ -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]
});
}
});
} }

View File

@ -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" })

View File

@ -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" });
}, },

View File

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

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

View 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,
};

View File

@ -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 ? (

View File

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

View 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 };