Compare commits
2 Commits
8f8bc0f541
...
a5533e914d
| Author | SHA1 | Date | |
|---|---|---|---|
| a5533e914d | |||
| 626345bc9d |
@ -1,12 +1,12 @@
|
|||||||
import axiosInstance from "./_axiosInstance";
|
import axiosInstance from "./_axiosInstance";
|
||||||
import { AuthLoginResponse, Pagination, QuestionResponse, UserResponse } from "./types";
|
import { AuthLoginResponse, CategoryResponse, Pagination, QuestionResponse, UserResponse } from "./types";
|
||||||
|
|
||||||
export const get_questions = async (page: number, test_id?: number, question_id?: number) => {
|
export const get_questions = async (page: number, test_id?: number, category_id?: number) => {
|
||||||
const response = await axiosInstance.get<Pagination<QuestionResponse>>("/api/questions/", {
|
const response = await axiosInstance.get<Pagination<QuestionResponse>>("/api/questions/", {
|
||||||
params: {
|
params: {
|
||||||
page,
|
page,
|
||||||
test_id,
|
test_id,
|
||||||
question_id
|
category_id
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -29,6 +29,14 @@ export const get_current_user = async () => {
|
|||||||
return response.data;
|
return response.data;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const get_categories = async () => {
|
||||||
|
const response = await axiosInstance.get<{
|
||||||
|
data: CategoryResponse[]
|
||||||
|
}>("/api/categories/");
|
||||||
|
|
||||||
|
return response.data;
|
||||||
|
}
|
||||||
|
|
||||||
export const post_login = async (email: string, password: string) => {
|
export const post_login = async (email: string, password: string) => {
|
||||||
const response = await axiosInstance.post<AuthLoginResponse>("/api/auth/login/", {
|
const response = await axiosInstance.post<AuthLoginResponse>("/api/auth/login/", {
|
||||||
email,
|
email,
|
||||||
|
|||||||
12
api/categories.ts
Normal file
12
api/categories.ts
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
import { useQuery } from "@tanstack/react-query";
|
||||||
|
import { get_categories } from "./_client";
|
||||||
|
|
||||||
|
export const useCategories = () => {
|
||||||
|
return useQuery({
|
||||||
|
queryKey: ['categories'],
|
||||||
|
queryFn: async () => {
|
||||||
|
const response = await get_categories();
|
||||||
|
return response.data;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
@ -4,12 +4,12 @@ import { get_question, get_questions } from "./_client";
|
|||||||
interface useQuestionsAttr {
|
interface useQuestionsAttr {
|
||||||
page?: number;
|
page?: number;
|
||||||
test_id?: number;
|
test_id?: number;
|
||||||
question_id?: number;
|
category_id?: number;
|
||||||
}
|
}
|
||||||
export const useQuestions = ({ page = 1, test_id, question_id }: useQuestionsAttr = { }) => {
|
export const useQuestions = ({ page = 1, test_id, category_id }: useQuestionsAttr = { }) => {
|
||||||
return useQuery({
|
return useQuery({
|
||||||
queryKey: ['questions', page, test_id, question_id],
|
queryKey: ['questions', page, test_id, category_id],
|
||||||
queryFn: () => get_questions(page, test_id, question_id)
|
queryFn: () => get_questions(page, test_id, category_id)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -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';
|
||||||
|
|||||||
@ -1,32 +1,66 @@
|
|||||||
import { View } from 'react-native';
|
import { ScrollView, View } from "react-native";
|
||||||
|
|
||||||
import { useQuestions } from '@/api';
|
import { useQuestions } from "@/api";
|
||||||
import Question from '@/components/question';
|
import { QuestionResponse } from "@/api/types";
|
||||||
import Content from '@/components/ui/content';
|
import useTaxonomyContext from "@/components/providers/taxonomy-provider/hook";
|
||||||
import { ThemedText } from '@/components/ui/themed-text';
|
import Question from "@/components/question";
|
||||||
import { router } from 'expo-router';
|
import Content from "@/components/ui/content";
|
||||||
|
import CustomSelect from "@/components/ui/custom-select";
|
||||||
|
import PaginationList from "@/components/ui/pagination-list";
|
||||||
|
import { ThemedText } from "@/components/ui/themed-text";
|
||||||
|
import { router } from "expo-router";
|
||||||
|
import { useRef, useState } from "react";
|
||||||
|
|
||||||
export default function QuestionsScreen() {
|
export default function QuestionsScreen() {
|
||||||
const { data: questions, isLoading: isLoadingQuestions } = useQuestions();
|
const [category, setCategory] = useState<undefined|number>(undefined);
|
||||||
|
const [page, setPage] = useState<number>(1);
|
||||||
|
const { categories } = useTaxonomyContext();
|
||||||
|
const scrollRef = useRef<ScrollView>(null);
|
||||||
|
const { data: questionsPagination, isLoading } = useQuestions({
|
||||||
|
page: page,
|
||||||
|
category_id: category
|
||||||
|
});
|
||||||
|
|
||||||
const questionsLoaded =
|
const categoryOptions = categories.map((cat) => {
|
||||||
!isLoadingQuestions && questions && questions.meta.total > 0;
|
return {
|
||||||
|
label: cat.name,
|
||||||
|
value: String(cat.id),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
const handleChangeCategory = (value: string) => {
|
||||||
|
setPage(1);
|
||||||
|
if(value === "all")
|
||||||
|
setCategory(undefined);
|
||||||
|
else
|
||||||
|
setCategory(+value);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Content>
|
<Content ref={scrollRef}>
|
||||||
<ThemedText type="title" className='mb-3'>Questions</ThemedText>
|
<ThemedText type="title" className="mb-3">
|
||||||
|
Questions
|
||||||
|
</ThemedText>
|
||||||
|
|
||||||
<View className='gap-3'>
|
<View className="mb-4">
|
||||||
{questionsLoaded &&
|
<CustomSelect
|
||||||
questions.data.map((question) => (
|
options={categoryOptions}
|
||||||
<Question
|
noneOption={{ label: "All categories", value: "all" }}
|
||||||
key={question.id}
|
selectedValue={category ? `${category}` : undefined}
|
||||||
question={question}
|
onValueChange={handleChangeCategory}
|
||||||
onPress={() => router.push(`/questions/${question.id}`)}
|
placeholder="Select category" />
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
|
<PaginationList<QuestionResponse>
|
||||||
|
pagination={questionsPagination}
|
||||||
|
renderItem={(item) => (
|
||||||
|
<Question question={item} onPress={() => router.push(`/questions/${item.id}`)} />
|
||||||
|
)}
|
||||||
|
skeleton={<Question />}
|
||||||
|
currentPage={page}
|
||||||
|
setCurrentPage={setPage}
|
||||||
|
scrollView={scrollRef}
|
||||||
|
isLoadingPage={isLoading}
|
||||||
|
/>
|
||||||
</Content>
|
</Content>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,50 +1,26 @@
|
|||||||
import {
|
|
||||||
DarkTheme,
|
|
||||||
DefaultTheme,
|
|
||||||
ThemeProvider,
|
|
||||||
} from "@react-navigation/native";
|
|
||||||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
|
||||||
import { Stack } from "expo-router";
|
import { Stack } from "expo-router";
|
||||||
import { StatusBar } from "expo-status-bar";
|
import { StatusBar } from "expo-status-bar";
|
||||||
import "react-native-reanimated";
|
import "react-native-reanimated";
|
||||||
|
|
||||||
import { useColorScheme } from "@/hooks/use-color-scheme";
|
|
||||||
|
|
||||||
import AuthProvider from "@/components/ui/auth-provider";
|
import IndexProvider from "@/components/providers/index-provider";
|
||||||
import { GluestackUIProvider } from "@/components/ui/gluestack-ui-provider";
|
|
||||||
import "@/global.css";
|
import "@/global.css";
|
||||||
import { ToastProvider } from 'react-native-toast-notifications';
|
|
||||||
|
|
||||||
export const unstable_settings = {
|
export const unstable_settings = {
|
||||||
anchor: "(tabs)",
|
anchor: "(tabs)",
|
||||||
};
|
};
|
||||||
|
|
||||||
const queryClient = new QueryClient();
|
|
||||||
|
|
||||||
export default function RootLayout() {
|
export default function RootLayout() {
|
||||||
const colorScheme = useColorScheme();
|
|
||||||
const mode = colorScheme === "dark" ? "dark" : "light";
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<GluestackUIProvider mode={mode}>
|
<IndexProvider>
|
||||||
<QueryClientProvider client={queryClient}>
|
<Stack>
|
||||||
<AuthProvider>
|
<Stack.Screen name="(tabs)" options={{ headerShown: false }} />
|
||||||
<ToastProvider>
|
<Stack.Screen
|
||||||
<ThemeProvider
|
name="modal"
|
||||||
value={colorScheme === "dark" ? DarkTheme : DefaultTheme}
|
options={{ presentation: "modal", title: "Modal" }}
|
||||||
>
|
/>
|
||||||
<Stack>
|
</Stack>
|
||||||
<Stack.Screen name="(tabs)" options={{ headerShown: false }} />
|
<StatusBar style="auto" />
|
||||||
<Stack.Screen
|
</IndexProvider>
|
||||||
name="modal"
|
|
||||||
options={{ presentation: "modal", title: "Modal" }}
|
|
||||||
/>
|
|
||||||
</Stack>
|
|
||||||
<StatusBar style="auto" />
|
|
||||||
</ThemeProvider>
|
|
||||||
</ToastProvider>
|
|
||||||
</AuthProvider>
|
|
||||||
</QueryClientProvider>
|
|
||||||
</GluestackUIProvider>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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>
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default QuestionScreen;
|
<Divider className="mt-5 mb-5" />
|
||||||
|
|
||||||
|
<View className="gap-4">
|
||||||
|
{displayAnswers &&
|
||||||
|
answers.map((answer) => (
|
||||||
|
<Answer
|
||||||
|
key={answer.id}
|
||||||
|
answer={answer}
|
||||||
|
isTextType={data.type === "text"}
|
||||||
|
correctAnswers={data.correct_answers}
|
||||||
|
displayCorrect={withCorrect}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
|
||||||
|
<Button
|
||||||
|
action="secondary"
|
||||||
|
onPress={() => setWithCorrect(!withCorrect)}
|
||||||
|
>
|
||||||
|
<ButtonText>Display answers: {withCorrectBtnText}</ButtonText>
|
||||||
|
</Button>
|
||||||
|
</View>
|
||||||
|
</Content>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default QuestionScreen;
|
||||||
|
|||||||
46
components/providers/index-provider/index.tsx
Normal file
46
components/providers/index-provider/index.tsx
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
import {
|
||||||
|
DarkTheme,
|
||||||
|
DefaultTheme,
|
||||||
|
ThemeProvider,
|
||||||
|
} from "@react-navigation/native";
|
||||||
|
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||||
|
import "react-native-reanimated";
|
||||||
|
|
||||||
|
import AuthProvider from "@/components/providers/auth-provider";
|
||||||
|
import { GluestackUIProvider } from "@/components/ui/gluestack-ui-provider";
|
||||||
|
import "@/global.css";
|
||||||
|
import { useColorScheme } from "@/hooks/use-color-scheme";
|
||||||
|
import { ReactNode } from "react";
|
||||||
|
import { ToastProvider } from "react-native-toast-notifications";
|
||||||
|
import TaxonomyProvider from "../taxonomy-provider";
|
||||||
|
|
||||||
|
interface IndexProviderProps {
|
||||||
|
children: ReactNode;
|
||||||
|
}
|
||||||
|
const queryClient = new QueryClient();
|
||||||
|
|
||||||
|
const IndexProvider = ({ children }: IndexProviderProps) => {
|
||||||
|
const colorScheme = useColorScheme();
|
||||||
|
const mode = colorScheme === "dark" ? "dark" : "light";
|
||||||
|
|
||||||
|
return (
|
||||||
|
<GluestackUIProvider mode={mode}>
|
||||||
|
<QueryClientProvider client={queryClient}>
|
||||||
|
<AuthProvider>
|
||||||
|
<TaxonomyProvider>
|
||||||
|
<ToastProvider>
|
||||||
|
<ThemeProvider
|
||||||
|
value={colorScheme === "dark" ? DarkTheme : DefaultTheme}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</ThemeProvider>
|
||||||
|
</ToastProvider>
|
||||||
|
</TaxonomyProvider>
|
||||||
|
</AuthProvider>
|
||||||
|
</QueryClientProvider>
|
||||||
|
</GluestackUIProvider>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
export default IndexProvider;
|
||||||
12
components/providers/taxonomy-provider/context.ts
Normal file
12
components/providers/taxonomy-provider/context.ts
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
import { CategoryResponse } from "@/api/types";
|
||||||
|
import { createContext } from "react";
|
||||||
|
|
||||||
|
interface TaxonomyContextType {
|
||||||
|
categories: CategoryResponse[];
|
||||||
|
}
|
||||||
|
|
||||||
|
const TaxonomyContext = createContext<TaxonomyContextType>({
|
||||||
|
categories: []
|
||||||
|
});
|
||||||
|
|
||||||
|
export default TaxonomyContext;
|
||||||
6
components/providers/taxonomy-provider/hook.ts
Normal file
6
components/providers/taxonomy-provider/hook.ts
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
import { useContext } from "react";
|
||||||
|
import TaxonomyContext from "./context";
|
||||||
|
|
||||||
|
const useTaxonomyContext = () => useContext(TaxonomyContext);
|
||||||
|
|
||||||
|
export default useTaxonomyContext;
|
||||||
29
components/providers/taxonomy-provider/index.tsx
Normal file
29
components/providers/taxonomy-provider/index.tsx
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
import { useCategories } from "@/api/categories";
|
||||||
|
import { CategoryResponse } from "@/api/types";
|
||||||
|
import { ReactNode, useMemo } from "react";
|
||||||
|
import TaxonomyContext from "./context";
|
||||||
|
|
||||||
|
interface TaxonomyProviderProps {
|
||||||
|
children: ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
const TaxonomyProvider = ({children}: TaxonomyProviderProps) => {
|
||||||
|
const { data: categories } = useCategories();
|
||||||
|
|
||||||
|
const cats = useMemo<CategoryResponse[]>(() => {
|
||||||
|
if(categories)
|
||||||
|
return categories;
|
||||||
|
|
||||||
|
return [];
|
||||||
|
}, [categories])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<TaxonomyContext.Provider value={{
|
||||||
|
categories: cats
|
||||||
|
}}>
|
||||||
|
{children}
|
||||||
|
</TaxonomyContext.Provider>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default TaxonomyProvider;
|
||||||
@ -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,14 +1,20 @@
|
|||||||
import { ContentPadding } from "@/constants/theme";
|
import { ContentPadding } from "@/constants/theme";
|
||||||
import { ReactNode } from "react";
|
import { forwardRef } from "react";
|
||||||
import { ScrollView, StyleSheet } from "react-native";
|
import { ScrollView, ScrollViewProps, StyleSheet } from "react-native";
|
||||||
|
|
||||||
interface ContentProps {
|
interface ContentProps extends ScrollViewProps {
|
||||||
children?: ReactNode
|
children?: React.ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
const Content = ({ children }: ContentProps) => {
|
const Content = forwardRef<ScrollView, ContentProps>(({ children, ...rest }, ref) => {
|
||||||
return <ScrollView style={styles.content}>{ children }</ScrollView>
|
return (
|
||||||
}
|
<ScrollView ref={ref} style={styles.content} {...rest}>
|
||||||
|
{children}
|
||||||
|
</ScrollView>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
Content.displayName = "Content";
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
const styles = StyleSheet.create({
|
||||||
content: {
|
content: {
|
||||||
|
|||||||
56
components/ui/custom-select/index.tsx
Normal file
56
components/ui/custom-select/index.tsx
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
import { useThemeColor } from "@/hooks/use-theme-color";
|
||||||
|
import { FontAwesome5 } from "@expo/vector-icons";
|
||||||
|
import { useMemo } from "react";
|
||||||
|
import { Select, SelectBackdrop, SelectContent, SelectDragIndicator, SelectDragIndicatorWrapper, SelectInput, SelectItem, SelectPortal, SelectTrigger } from "../select";
|
||||||
|
|
||||||
|
type SelectOption = {
|
||||||
|
label: string;
|
||||||
|
value: string;
|
||||||
|
}
|
||||||
|
interface CustomSelectProps {
|
||||||
|
placeholder?: string;
|
||||||
|
options: SelectOption[],
|
||||||
|
noneOption?: SelectOption;
|
||||||
|
defaultValue?: string;
|
||||||
|
selectedValue?:string;
|
||||||
|
onValueChange?: (value: string) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const CustomSelect = ({
|
||||||
|
options,
|
||||||
|
onValueChange,
|
||||||
|
defaultValue,
|
||||||
|
selectedValue,
|
||||||
|
noneOption,
|
||||||
|
placeholder = "Select option"
|
||||||
|
}: CustomSelectProps) => {
|
||||||
|
const optionsComputed = useMemo(() => {
|
||||||
|
if(noneOption)
|
||||||
|
return [noneOption, ...options];
|
||||||
|
|
||||||
|
return options;
|
||||||
|
}, [options, noneOption]);
|
||||||
|
const backgroundColor = useThemeColor({ }, "background");
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Select onValueChange={onValueChange} selectedValue={selectedValue} defaultValue={defaultValue}>
|
||||||
|
<SelectTrigger variant="outline" size="md" style={{ backgroundColor }}>
|
||||||
|
<SelectInput placeholder={placeholder} />
|
||||||
|
<FontAwesome5 className="mr-3" name="chevron-down" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectPortal>
|
||||||
|
<SelectBackdrop />
|
||||||
|
<SelectContent>
|
||||||
|
<SelectDragIndicatorWrapper>
|
||||||
|
<SelectDragIndicator />
|
||||||
|
</SelectDragIndicatorWrapper>
|
||||||
|
{optionsComputed.map((opt, i) => (
|
||||||
|
<SelectItem key={i} label={opt.label} value={opt.value} />
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</SelectPortal>
|
||||||
|
</Select>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default CustomSelect;
|
||||||
86
components/ui/pagination-list/index.tsx
Normal file
86
components/ui/pagination-list/index.tsx
Normal file
@ -0,0 +1,86 @@
|
|||||||
|
import { Pagination } from "@/api/types";
|
||||||
|
import { ReactElement, RefObject } from "react";
|
||||||
|
import { ScrollView, View } from "react-native";
|
||||||
|
import { Button, ButtonText } from "../button";
|
||||||
|
import { ThemedText } from "../themed-text";
|
||||||
|
|
||||||
|
type CustomFlatListProps<T> = {
|
||||||
|
pagination?: Pagination<T>;
|
||||||
|
skeleton: ReactElement;
|
||||||
|
skeletonStartCount?: number;
|
||||||
|
currentPage: number;
|
||||||
|
setCurrentPage: (v: number) => void;
|
||||||
|
isLoadingPage: boolean;
|
||||||
|
renderItem: (item: T) => ReactElement;
|
||||||
|
pagesRadius?: number;
|
||||||
|
dividerHeight?: number;
|
||||||
|
scrollView?: RefObject<ScrollView | null>;
|
||||||
|
};
|
||||||
|
|
||||||
|
const PaginationList = <T,>({
|
||||||
|
pagination,
|
||||||
|
skeleton,
|
||||||
|
currentPage,
|
||||||
|
setCurrentPage,
|
||||||
|
isLoadingPage,
|
||||||
|
renderItem,
|
||||||
|
scrollView,
|
||||||
|
pagesRadius = 3,
|
||||||
|
skeletonStartCount = 4,
|
||||||
|
dividerHeight = 16,
|
||||||
|
}: CustomFlatListProps<T>) => {
|
||||||
|
const renderSkeleton = () => {
|
||||||
|
const skeletonCount = pagination
|
||||||
|
? pagination.meta.per_page
|
||||||
|
: skeletonStartCount;
|
||||||
|
return Array.from({ length: skeletonCount }).map((_, i) => (
|
||||||
|
<View key={i} style={{ marginBottom: dividerHeight }}>
|
||||||
|
{skeleton}
|
||||||
|
</View>
|
||||||
|
));
|
||||||
|
};
|
||||||
|
const handlePagePress = (page: number) => {
|
||||||
|
if (page === currentPage) return;
|
||||||
|
setCurrentPage(page);
|
||||||
|
scrollView?.current?.scrollTo({ y: 0, animated: false });
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!pagination || isLoadingPage) return renderSkeleton();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View>
|
||||||
|
{pagination.data.map((item, index) => (
|
||||||
|
<View key={index}>
|
||||||
|
{renderItem(item)}
|
||||||
|
<View style={{ height: dividerHeight }} />
|
||||||
|
</View>
|
||||||
|
))}
|
||||||
|
{pagination.data.length === 0 && (
|
||||||
|
<ThemedText className="pt-4 text-center">No items found.</ThemedText>
|
||||||
|
)}
|
||||||
|
<View className="flex-row gap-3">
|
||||||
|
{currentPage > 1 && (
|
||||||
|
<Button
|
||||||
|
className="flex-1"
|
||||||
|
onPress={() => handlePagePress(currentPage - 1)}
|
||||||
|
action="secondary"
|
||||||
|
>
|
||||||
|
<ButtonText>Prev</ButtonText>
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{currentPage !== pagination.meta.last_page && (
|
||||||
|
<Button
|
||||||
|
className="flex-1"
|
||||||
|
onPress={() => handlePagePress(currentPage + 1)}
|
||||||
|
action="secondary"
|
||||||
|
>
|
||||||
|
<ButtonText>Next</ButtonText>
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default PaginationList;
|
||||||
@ -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>
|
||||||
)
|
)
|
||||||
|
|||||||
277
components/ui/select/index.tsx
Normal file
277
components/ui/select/index.tsx
Normal file
@ -0,0 +1,277 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import React from 'react';
|
||||||
|
import { tva } from '@gluestack-ui/utils/nativewind-utils';
|
||||||
|
import { PrimitiveIcon, UIIcon } from '@gluestack-ui/core/icon/creator';
|
||||||
|
import {
|
||||||
|
withStyleContext,
|
||||||
|
useStyleContext,
|
||||||
|
} from '@gluestack-ui/utils/nativewind-utils';
|
||||||
|
import type { VariantProps } from '@gluestack-ui/utils/nativewind-utils';
|
||||||
|
import { createSelect } from '@gluestack-ui/core/select/creator';
|
||||||
|
import { cssInterop } from 'nativewind';
|
||||||
|
import {
|
||||||
|
Actionsheet,
|
||||||
|
ActionsheetContent,
|
||||||
|
ActionsheetItem,
|
||||||
|
ActionsheetItemText,
|
||||||
|
ActionsheetDragIndicator,
|
||||||
|
ActionsheetDragIndicatorWrapper,
|
||||||
|
ActionsheetBackdrop,
|
||||||
|
ActionsheetScrollView,
|
||||||
|
ActionsheetVirtualizedList,
|
||||||
|
ActionsheetFlatList,
|
||||||
|
ActionsheetSectionList,
|
||||||
|
ActionsheetSectionHeaderText,
|
||||||
|
} from './select-actionsheet';
|
||||||
|
import { Pressable, View, TextInput } from 'react-native';
|
||||||
|
|
||||||
|
const SelectTriggerWrapper = React.forwardRef<
|
||||||
|
React.ComponentRef<typeof Pressable>,
|
||||||
|
React.ComponentProps<typeof Pressable>
|
||||||
|
>(function SelectTriggerWrapper({ ...props }, ref) {
|
||||||
|
return <Pressable {...props} ref={ref} />;
|
||||||
|
});
|
||||||
|
|
||||||
|
const selectIconStyle = tva({
|
||||||
|
base: 'text-background-500 fill-none',
|
||||||
|
parentVariants: {
|
||||||
|
size: {
|
||||||
|
'2xs': 'h-3 w-3',
|
||||||
|
'xs': 'h-3.5 w-3.5',
|
||||||
|
'sm': 'h-4 w-4',
|
||||||
|
'md': 'h-[18px] w-[18px]',
|
||||||
|
'lg': 'h-5 w-5',
|
||||||
|
'xl': 'h-6 w-6',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const selectStyle = tva({
|
||||||
|
base: '',
|
||||||
|
});
|
||||||
|
|
||||||
|
const selectTriggerStyle = tva({
|
||||||
|
base: 'border border-background-300 rounded flex-row items-center overflow-hidden data-[hover=true]:border-outline-400 data-[focus=true]:border-primary-700 data-[disabled=true]:opacity-40 data-[disabled=true]:data-[hover=true]:border-background-300',
|
||||||
|
variants: {
|
||||||
|
size: {
|
||||||
|
xl: 'h-12',
|
||||||
|
lg: 'h-11',
|
||||||
|
md: 'h-10',
|
||||||
|
sm: 'h-9',
|
||||||
|
},
|
||||||
|
variant: {
|
||||||
|
underlined:
|
||||||
|
'border-0 border-b rounded-none data-[hover=true]:border-primary-700 data-[focus=true]:border-primary-700 data-[focus=true]:web:shadow-[inset_0_-1px_0_0] data-[focus=true]:web:shadow-primary-700 data-[invalid=true]:border-error-700 data-[invalid=true]:web:shadow-error-700',
|
||||||
|
outline:
|
||||||
|
'data-[focus=true]:border-primary-700 data-[focus=true]:web:shadow-[inset_0_0_0_1px] data-[focus=true]:data-[hover=true]:web:shadow-primary-600 data-[invalid=true]:web:shadow-[inset_0_0_0_1px] data-[invalid=true]:border-error-700 data-[invalid=true]:web:shadow-error-700 data-[invalid=true]:data-[hover=true]:border-error-700',
|
||||||
|
rounded:
|
||||||
|
'rounded-full data-[focus=true]:border-primary-700 data-[focus=true]:web:shadow-[inset_0_0_0_1px] data-[focus=true]:web:shadow-primary-700 data-[invalid=true]:border-error-700 data-[invalid=true]:web:shadow-error-700',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const selectInputStyle = tva({
|
||||||
|
base: 'px-3 placeholder:text-typography-500 web:w-full h-full text-typography-900 pointer-events-none web:outline-none ios:leading-[0px] py-0',
|
||||||
|
parentVariants: {
|
||||||
|
size: {
|
||||||
|
xl: 'text-xl',
|
||||||
|
lg: 'text-lg',
|
||||||
|
md: 'text-base',
|
||||||
|
sm: 'text-sm',
|
||||||
|
},
|
||||||
|
variant: {
|
||||||
|
underlined: 'px-0',
|
||||||
|
outline: '',
|
||||||
|
rounded: 'px-4',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const UISelect = createSelect(
|
||||||
|
{
|
||||||
|
Root: View,
|
||||||
|
Trigger: withStyleContext(SelectTriggerWrapper),
|
||||||
|
Input: TextInput,
|
||||||
|
Icon: UIIcon,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Portal: Actionsheet,
|
||||||
|
Backdrop: ActionsheetBackdrop,
|
||||||
|
Content: ActionsheetContent,
|
||||||
|
DragIndicator: ActionsheetDragIndicator,
|
||||||
|
DragIndicatorWrapper: ActionsheetDragIndicatorWrapper,
|
||||||
|
Item: ActionsheetItem,
|
||||||
|
ItemText: ActionsheetItemText,
|
||||||
|
ScrollView: ActionsheetScrollView,
|
||||||
|
VirtualizedList: ActionsheetVirtualizedList,
|
||||||
|
FlatList: ActionsheetFlatList,
|
||||||
|
SectionList: ActionsheetSectionList,
|
||||||
|
SectionHeaderText: ActionsheetSectionHeaderText,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
cssInterop(UISelect, { className: 'style' });
|
||||||
|
cssInterop(UISelect.Input, {
|
||||||
|
className: { target: 'style', nativeStyleToProp: { textAlign: true } },
|
||||||
|
});
|
||||||
|
cssInterop(SelectTriggerWrapper, { className: 'style' });
|
||||||
|
|
||||||
|
cssInterop(PrimitiveIcon, {
|
||||||
|
className: {
|
||||||
|
target: 'style',
|
||||||
|
nativeStyleToProp: {
|
||||||
|
height: true,
|
||||||
|
width: true,
|
||||||
|
fill: true,
|
||||||
|
color: 'classNameColor',
|
||||||
|
stroke: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
type ISelectProps = VariantProps<typeof selectStyle> &
|
||||||
|
React.ComponentProps<typeof UISelect> & { className?: string };
|
||||||
|
|
||||||
|
const Select = React.forwardRef<
|
||||||
|
React.ComponentRef<typeof UISelect>,
|
||||||
|
ISelectProps
|
||||||
|
>(function Select({ className, ...props }, ref) {
|
||||||
|
return (
|
||||||
|
<UISelect
|
||||||
|
className={selectStyle({
|
||||||
|
class: className,
|
||||||
|
})}
|
||||||
|
ref={ref}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
type ISelectTriggerProps = VariantProps<typeof selectTriggerStyle> &
|
||||||
|
React.ComponentProps<typeof UISelect.Trigger> & { className?: string };
|
||||||
|
|
||||||
|
const SelectTrigger = React.forwardRef<
|
||||||
|
React.ComponentRef<typeof UISelect.Trigger>,
|
||||||
|
ISelectTriggerProps
|
||||||
|
>(function SelectTrigger(
|
||||||
|
{ className, size = 'md', variant = 'outline', ...props },
|
||||||
|
ref
|
||||||
|
) {
|
||||||
|
return (
|
||||||
|
<UISelect.Trigger
|
||||||
|
className={selectTriggerStyle({
|
||||||
|
class: className,
|
||||||
|
size,
|
||||||
|
variant,
|
||||||
|
})}
|
||||||
|
ref={ref}
|
||||||
|
context={{ size, variant }}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
type ISelectInputProps = VariantProps<typeof selectInputStyle> &
|
||||||
|
React.ComponentProps<typeof UISelect.Input> & { className?: string };
|
||||||
|
|
||||||
|
const SelectInput = React.forwardRef<
|
||||||
|
React.ComponentRef<typeof UISelect.Input>,
|
||||||
|
ISelectInputProps
|
||||||
|
>(function SelectInput({ className, ...props }, ref) {
|
||||||
|
const { size: parentSize, variant: parentVariant } = useStyleContext();
|
||||||
|
return (
|
||||||
|
<UISelect.Input
|
||||||
|
className={selectInputStyle({
|
||||||
|
class: className,
|
||||||
|
parentVariants: {
|
||||||
|
size: parentSize,
|
||||||
|
variant: parentVariant,
|
||||||
|
},
|
||||||
|
})}
|
||||||
|
ref={ref}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
type ISelectIcon = VariantProps<typeof selectIconStyle> &
|
||||||
|
React.ComponentProps<typeof UISelect.Icon> & { className?: string };
|
||||||
|
|
||||||
|
const SelectIcon = React.forwardRef<
|
||||||
|
React.ComponentRef<typeof UISelect.Icon>,
|
||||||
|
ISelectIcon
|
||||||
|
>(function SelectIcon({ className, size, ...props }, ref) {
|
||||||
|
const { size: parentSize } = useStyleContext();
|
||||||
|
if (typeof size === 'number') {
|
||||||
|
return (
|
||||||
|
<UISelect.Icon
|
||||||
|
ref={ref}
|
||||||
|
{...props}
|
||||||
|
className={selectIconStyle({ class: className })}
|
||||||
|
size={size}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
} else if (
|
||||||
|
//@ts-expect-error : web only
|
||||||
|
(props?.height !== undefined || props?.width !== undefined) &&
|
||||||
|
size === undefined
|
||||||
|
) {
|
||||||
|
return (
|
||||||
|
<UISelect.Icon
|
||||||
|
ref={ref}
|
||||||
|
{...props}
|
||||||
|
className={selectIconStyle({ class: className })}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<UISelect.Icon
|
||||||
|
className={selectIconStyle({
|
||||||
|
class: className,
|
||||||
|
size,
|
||||||
|
parentVariants: {
|
||||||
|
size: parentSize,
|
||||||
|
},
|
||||||
|
})}
|
||||||
|
ref={ref}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
Select.displayName = 'Select';
|
||||||
|
SelectTrigger.displayName = 'SelectTrigger';
|
||||||
|
SelectInput.displayName = 'SelectInput';
|
||||||
|
SelectIcon.displayName = 'SelectIcon';
|
||||||
|
|
||||||
|
// Actionsheet Components
|
||||||
|
const SelectPortal = UISelect.Portal;
|
||||||
|
const SelectBackdrop = UISelect.Backdrop;
|
||||||
|
const SelectContent = UISelect.Content;
|
||||||
|
const SelectDragIndicator = UISelect.DragIndicator;
|
||||||
|
const SelectDragIndicatorWrapper = UISelect.DragIndicatorWrapper;
|
||||||
|
const SelectItem = UISelect.Item;
|
||||||
|
const SelectScrollView = UISelect.ScrollView;
|
||||||
|
const SelectVirtualizedList = UISelect.VirtualizedList;
|
||||||
|
const SelectFlatList = UISelect.FlatList;
|
||||||
|
const SelectSectionList = UISelect.SectionList;
|
||||||
|
const SelectSectionHeaderText = UISelect.SectionHeaderText;
|
||||||
|
|
||||||
|
export {
|
||||||
|
Select,
|
||||||
|
SelectTrigger,
|
||||||
|
SelectInput,
|
||||||
|
SelectIcon,
|
||||||
|
SelectPortal,
|
||||||
|
SelectBackdrop,
|
||||||
|
SelectContent,
|
||||||
|
SelectDragIndicator,
|
||||||
|
SelectDragIndicatorWrapper,
|
||||||
|
SelectItem,
|
||||||
|
SelectScrollView,
|
||||||
|
SelectVirtualizedList,
|
||||||
|
SelectFlatList,
|
||||||
|
SelectSectionList,
|
||||||
|
SelectSectionHeaderText,
|
||||||
|
};
|
||||||
562
components/ui/select/select-actionsheet.tsx
Normal file
562
components/ui/select/select-actionsheet.tsx
Normal file
@ -0,0 +1,562 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { H4 } from '@expo/html-elements';
|
||||||
|
import { createActionsheet } from '@gluestack-ui/core/actionsheet/creator';
|
||||||
|
import {
|
||||||
|
Pressable,
|
||||||
|
View,
|
||||||
|
Text,
|
||||||
|
ScrollView,
|
||||||
|
VirtualizedList,
|
||||||
|
FlatList,
|
||||||
|
SectionList,
|
||||||
|
ViewStyle,
|
||||||
|
} from 'react-native';
|
||||||
|
import { PrimitiveIcon, UIIcon } from '@gluestack-ui/core/icon/creator';
|
||||||
|
import { tva } from '@gluestack-ui/utils/nativewind-utils';
|
||||||
|
import type { VariantProps } from '@gluestack-ui/utils/nativewind-utils';
|
||||||
|
import { withStyleContext } from '@gluestack-ui/utils/nativewind-utils';
|
||||||
|
import { cssInterop } from 'nativewind';
|
||||||
|
import {
|
||||||
|
Motion,
|
||||||
|
AnimatePresence,
|
||||||
|
createMotionAnimatedComponent,
|
||||||
|
MotionComponentProps,
|
||||||
|
} from '@legendapp/motion';
|
||||||
|
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
type IAnimatedPressableProps = React.ComponentProps<typeof Pressable> &
|
||||||
|
MotionComponentProps<typeof Pressable, ViewStyle, unknown, unknown, unknown>;
|
||||||
|
|
||||||
|
const AnimatedPressable = createMotionAnimatedComponent(
|
||||||
|
Pressable
|
||||||
|
) as React.ComponentType<IAnimatedPressableProps>;
|
||||||
|
|
||||||
|
type IMotionViewProps = React.ComponentProps<typeof View> &
|
||||||
|
MotionComponentProps<typeof View, ViewStyle, unknown, unknown, unknown>;
|
||||||
|
|
||||||
|
const MotionView = Motion.View as React.ComponentType<IMotionViewProps>;
|
||||||
|
|
||||||
|
export const UIActionsheet = createActionsheet({
|
||||||
|
Root: View,
|
||||||
|
Content: withStyleContext(MotionView),
|
||||||
|
Item: withStyleContext(Pressable),
|
||||||
|
ItemText: Text,
|
||||||
|
DragIndicator: View,
|
||||||
|
IndicatorWrapper: View,
|
||||||
|
Backdrop: AnimatedPressable,
|
||||||
|
ScrollView: ScrollView,
|
||||||
|
VirtualizedList: VirtualizedList,
|
||||||
|
FlatList: FlatList,
|
||||||
|
SectionList: SectionList,
|
||||||
|
SectionHeaderText: H4,
|
||||||
|
Icon: UIIcon,
|
||||||
|
AnimatePresence: AnimatePresence,
|
||||||
|
});
|
||||||
|
|
||||||
|
cssInterop(UIActionsheet, { className: 'style' });
|
||||||
|
cssInterop(UIActionsheet.Content, { className: 'style' });
|
||||||
|
cssInterop(UIActionsheet.Item, { className: 'style' });
|
||||||
|
cssInterop(UIActionsheet.ItemText, { className: 'style' });
|
||||||
|
cssInterop(UIActionsheet.DragIndicator, { className: 'style' });
|
||||||
|
cssInterop(UIActionsheet.DragIndicatorWrapper, { className: 'style' });
|
||||||
|
cssInterop(UIActionsheet.Backdrop, { className: 'style' });
|
||||||
|
cssInterop(UIActionsheet.ScrollView, {
|
||||||
|
className: 'style',
|
||||||
|
contentContainerClassName: 'contentContainerStyle',
|
||||||
|
indicatorClassName: 'indicatorStyle',
|
||||||
|
});
|
||||||
|
cssInterop(UIActionsheet.VirtualizedList, {
|
||||||
|
className: 'style',
|
||||||
|
ListFooterComponentClassName: 'ListFooterComponentStyle',
|
||||||
|
ListHeaderComponentClassName: 'ListHeaderComponentStyle',
|
||||||
|
contentContainerClassName: 'contentContainerStyle',
|
||||||
|
indicatorClassName: 'indicatorStyle',
|
||||||
|
});
|
||||||
|
cssInterop(UIActionsheet.FlatList, {
|
||||||
|
className: 'style',
|
||||||
|
ListFooterComponentClassName: 'ListFooterComponentStyle',
|
||||||
|
ListHeaderComponentClassName: 'ListHeaderComponentStyle',
|
||||||
|
columnWrapperClassName: 'columnWrapperStyle',
|
||||||
|
contentContainerClassName: 'contentContainerStyle',
|
||||||
|
indicatorClassName: 'indicatorStyle',
|
||||||
|
});
|
||||||
|
cssInterop(UIActionsheet.SectionList, { className: 'style' });
|
||||||
|
cssInterop(UIActionsheet.SectionHeaderText, { className: 'style' });
|
||||||
|
cssInterop(PrimitiveIcon, {
|
||||||
|
className: {
|
||||||
|
target: 'style',
|
||||||
|
nativeStyleToProp: {
|
||||||
|
height: true,
|
||||||
|
width: true,
|
||||||
|
fill: true,
|
||||||
|
color: 'classNameColor',
|
||||||
|
stroke: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const actionsheetStyle = tva({ base: 'w-full h-full web:pointer-events-none' });
|
||||||
|
|
||||||
|
const actionsheetContentStyle = tva({
|
||||||
|
base: 'items-center rounded-tl-3xl rounded-tr-3xl p-2 bg-background-0 web:pointer-events-auto web:select-none shadow-lg pb-safe',
|
||||||
|
});
|
||||||
|
|
||||||
|
const actionsheetItemStyle = tva({
|
||||||
|
base: 'w-full flex-row items-center p-3 rounded-sm data-[disabled=true]:opacity-40 data-[disabled=true]:web:pointer-events-auto data-[disabled=true]:web:cursor-not-allowed hover:bg-background-50 active:bg-background-100 data-[focus=true]:bg-background-100 web:data-[focus-visible=true]:bg-background-100 data-[checked=true]:bg-background-100',
|
||||||
|
});
|
||||||
|
|
||||||
|
const actionsheetItemTextStyle = tva({
|
||||||
|
base: 'text-typography-700 font-normal font-body tracking-md text-left mx-2',
|
||||||
|
variants: {
|
||||||
|
isTruncated: {
|
||||||
|
true: '',
|
||||||
|
},
|
||||||
|
bold: {
|
||||||
|
true: 'font-bold',
|
||||||
|
},
|
||||||
|
underline: {
|
||||||
|
true: 'underline',
|
||||||
|
},
|
||||||
|
strikeThrough: {
|
||||||
|
true: 'line-through',
|
||||||
|
},
|
||||||
|
size: {
|
||||||
|
'2xs': 'text-2xs',
|
||||||
|
'xs': 'text-xs',
|
||||||
|
'sm': 'text-sm',
|
||||||
|
'md': 'text-base',
|
||||||
|
'lg': 'text-lg',
|
||||||
|
'xl': 'text-xl',
|
||||||
|
'2xl': 'text-2xl',
|
||||||
|
'3xl': 'text-3xl',
|
||||||
|
'4xl': 'text-4xl',
|
||||||
|
'5xl': 'text-5xl',
|
||||||
|
'6xl': 'text-6xl',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
defaultVariants: {
|
||||||
|
size: 'md',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const actionsheetDragIndicatorStyle = tva({
|
||||||
|
base: 'w-16 h-1 bg-background-400 rounded-full',
|
||||||
|
});
|
||||||
|
|
||||||
|
const actionsheetDragIndicatorWrapperStyle = tva({
|
||||||
|
base: 'w-full py-1 items-center',
|
||||||
|
});
|
||||||
|
|
||||||
|
const actionsheetBackdropStyle = tva({
|
||||||
|
base: 'absolute left-0 top-0 right-0 bottom-0 bg-background-dark web:cursor-default web:pointer-events-auto',
|
||||||
|
});
|
||||||
|
|
||||||
|
const actionsheetScrollViewStyle = tva({
|
||||||
|
base: 'w-full h-auto',
|
||||||
|
});
|
||||||
|
|
||||||
|
const actionsheetVirtualizedListStyle = tva({
|
||||||
|
base: 'w-full h-auto',
|
||||||
|
});
|
||||||
|
|
||||||
|
const actionsheetFlatListStyle = tva({
|
||||||
|
base: 'w-full h-auto',
|
||||||
|
});
|
||||||
|
|
||||||
|
const actionsheetSectionListStyle = tva({
|
||||||
|
base: 'w-full h-auto',
|
||||||
|
});
|
||||||
|
|
||||||
|
const actionsheetSectionHeaderTextStyle = tva({
|
||||||
|
base: 'leading-5 font-bold font-heading my-0 text-typography-500 p-3 uppercase',
|
||||||
|
variants: {
|
||||||
|
isTruncated: {
|
||||||
|
true: '',
|
||||||
|
},
|
||||||
|
bold: {
|
||||||
|
true: 'font-bold',
|
||||||
|
},
|
||||||
|
underline: {
|
||||||
|
true: 'underline',
|
||||||
|
},
|
||||||
|
strikeThrough: {
|
||||||
|
true: 'line-through',
|
||||||
|
},
|
||||||
|
size: {
|
||||||
|
'5xl': 'text-5xl',
|
||||||
|
'4xl': 'text-4xl',
|
||||||
|
'3xl': 'text-3xl',
|
||||||
|
'2xl': 'text-2xl',
|
||||||
|
'xl': 'text-xl',
|
||||||
|
'lg': 'text-lg',
|
||||||
|
'md': 'text-base',
|
||||||
|
'sm': 'text-sm',
|
||||||
|
'xs': 'text-xs',
|
||||||
|
},
|
||||||
|
|
||||||
|
sub: {
|
||||||
|
true: 'text-xs',
|
||||||
|
},
|
||||||
|
italic: {
|
||||||
|
true: 'italic',
|
||||||
|
},
|
||||||
|
highlight: {
|
||||||
|
true: 'bg-yellow500',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
defaultVariants: {
|
||||||
|
size: 'xs',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const actionsheetIconStyle = tva({
|
||||||
|
base: 'text-typography-900',
|
||||||
|
variants: {
|
||||||
|
size: {
|
||||||
|
'2xs': 'h-3 w-3',
|
||||||
|
'xs': 'h-3.5 w-3.5',
|
||||||
|
'sm': 'h-4 w-4',
|
||||||
|
'md': 'w-4 h-4',
|
||||||
|
'lg': 'h-5 w-5',
|
||||||
|
'xl': 'h-6 w-6',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
type IActionsheetProps = VariantProps<typeof actionsheetStyle> &
|
||||||
|
React.ComponentProps<typeof UIActionsheet> & { className?: string };
|
||||||
|
|
||||||
|
type IActionsheetContentProps = VariantProps<typeof actionsheetContentStyle> &
|
||||||
|
React.ComponentProps<typeof UIActionsheet.Content> & { className?: string };
|
||||||
|
|
||||||
|
type IActionsheetItemProps = VariantProps<typeof actionsheetItemStyle> &
|
||||||
|
React.ComponentProps<typeof UIActionsheet.Item> & { className?: string };
|
||||||
|
|
||||||
|
type IActionsheetItemTextProps = VariantProps<typeof actionsheetItemTextStyle> &
|
||||||
|
React.ComponentProps<typeof UIActionsheet.ItemText> & { className?: string };
|
||||||
|
|
||||||
|
type IActionsheetDragIndicatorProps = VariantProps<
|
||||||
|
typeof actionsheetDragIndicatorStyle
|
||||||
|
> &
|
||||||
|
React.ComponentProps<typeof UIActionsheet.DragIndicator> & {
|
||||||
|
className?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
type IActionsheetDragIndicatorWrapperProps = VariantProps<
|
||||||
|
typeof actionsheetDragIndicatorWrapperStyle
|
||||||
|
> &
|
||||||
|
React.ComponentProps<typeof UIActionsheet.DragIndicatorWrapper> & {
|
||||||
|
className?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
type IActionsheetBackdropProps = VariantProps<typeof actionsheetBackdropStyle> &
|
||||||
|
React.ComponentProps<typeof UIActionsheet.Backdrop> & {
|
||||||
|
className?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
type IActionsheetScrollViewProps = VariantProps<
|
||||||
|
typeof actionsheetScrollViewStyle
|
||||||
|
> &
|
||||||
|
React.ComponentProps<typeof UIActionsheet.ScrollView> & {
|
||||||
|
className?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
type IActionsheetVirtualizedListProps = VariantProps<
|
||||||
|
typeof actionsheetVirtualizedListStyle
|
||||||
|
> &
|
||||||
|
React.ComponentProps<typeof UIActionsheet.VirtualizedList> & {
|
||||||
|
className?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
type IActionsheetFlatListProps = VariantProps<typeof actionsheetFlatListStyle> &
|
||||||
|
React.ComponentProps<typeof UIActionsheet.FlatList> & {
|
||||||
|
className?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
type IActionsheetSectionListProps = VariantProps<
|
||||||
|
typeof actionsheetSectionListStyle
|
||||||
|
> &
|
||||||
|
React.ComponentProps<typeof UIActionsheet.SectionList> & {
|
||||||
|
className?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
type IActionsheetSectionHeaderTextProps = VariantProps<
|
||||||
|
typeof actionsheetSectionHeaderTextStyle
|
||||||
|
> &
|
||||||
|
React.ComponentProps<typeof UIActionsheet.SectionHeaderText> & {
|
||||||
|
className?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
type IActionsheetIconProps = VariantProps<typeof actionsheetIconStyle> &
|
||||||
|
React.ComponentProps<typeof UIActionsheet.Icon> & {
|
||||||
|
className?: string;
|
||||||
|
as?: React.ElementType;
|
||||||
|
};
|
||||||
|
|
||||||
|
const Actionsheet = React.forwardRef<
|
||||||
|
React.ComponentRef<typeof UIActionsheet>,
|
||||||
|
IActionsheetProps
|
||||||
|
>(function Actionsheet({ className, ...props }, ref) {
|
||||||
|
return (
|
||||||
|
<UIActionsheet
|
||||||
|
className={actionsheetStyle({
|
||||||
|
class: className,
|
||||||
|
})}
|
||||||
|
ref={ref}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
const ActionsheetContent = React.forwardRef<
|
||||||
|
React.ComponentRef<typeof UIActionsheet.Content>,
|
||||||
|
IActionsheetContentProps & { className?: string }
|
||||||
|
>(function ActionsheetContent({ className, ...props }, ref) {
|
||||||
|
return (
|
||||||
|
<UIActionsheet.Content
|
||||||
|
className={actionsheetContentStyle({
|
||||||
|
class: className,
|
||||||
|
})}
|
||||||
|
ref={ref}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
const ActionsheetItem = React.forwardRef<
|
||||||
|
React.ComponentRef<typeof UIActionsheet.Item>,
|
||||||
|
IActionsheetItemProps
|
||||||
|
>(function ActionsheetItem({ className, ...props }, ref) {
|
||||||
|
return (
|
||||||
|
<UIActionsheet.Item
|
||||||
|
className={actionsheetItemStyle({
|
||||||
|
class: className,
|
||||||
|
})}
|
||||||
|
ref={ref}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
const ActionsheetItemText = React.forwardRef<
|
||||||
|
React.ComponentRef<typeof UIActionsheet.ItemText>,
|
||||||
|
IActionsheetItemTextProps
|
||||||
|
>(function ActionsheetItemText(
|
||||||
|
{ className, isTruncated, bold, underline, strikeThrough, size, ...props },
|
||||||
|
ref
|
||||||
|
) {
|
||||||
|
return (
|
||||||
|
<UIActionsheet.ItemText
|
||||||
|
className={actionsheetItemTextStyle({
|
||||||
|
class: className,
|
||||||
|
isTruncated: isTruncated as boolean,
|
||||||
|
bold: bold as boolean,
|
||||||
|
underline: underline as boolean,
|
||||||
|
strikeThrough: strikeThrough as boolean,
|
||||||
|
size,
|
||||||
|
})}
|
||||||
|
ref={ref}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
const ActionsheetDragIndicator = React.forwardRef<
|
||||||
|
React.ComponentRef<typeof UIActionsheet.DragIndicator>,
|
||||||
|
IActionsheetDragIndicatorProps
|
||||||
|
>(function ActionsheetDragIndicator({ className, ...props }, ref) {
|
||||||
|
return (
|
||||||
|
<UIActionsheet.DragIndicator
|
||||||
|
className={actionsheetDragIndicatorStyle({
|
||||||
|
class: className,
|
||||||
|
})}
|
||||||
|
ref={ref}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
const ActionsheetDragIndicatorWrapper = React.forwardRef<
|
||||||
|
React.ComponentRef<typeof UIActionsheet.DragIndicatorWrapper>,
|
||||||
|
IActionsheetDragIndicatorWrapperProps
|
||||||
|
>(function ActionsheetDragIndicatorWrapper({ className, ...props }, ref) {
|
||||||
|
return (
|
||||||
|
<UIActionsheet.DragIndicatorWrapper
|
||||||
|
className={actionsheetDragIndicatorWrapperStyle({
|
||||||
|
class: className,
|
||||||
|
})}
|
||||||
|
ref={ref}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
const ActionsheetBackdrop = React.forwardRef<
|
||||||
|
React.ComponentRef<typeof UIActionsheet.Backdrop>,
|
||||||
|
IActionsheetBackdropProps
|
||||||
|
>(function ActionsheetBackdrop({ className, ...props }, ref) {
|
||||||
|
return (
|
||||||
|
<UIActionsheet.Backdrop
|
||||||
|
initial={{
|
||||||
|
opacity: 0,
|
||||||
|
}}
|
||||||
|
animate={{
|
||||||
|
opacity: 0.5,
|
||||||
|
}}
|
||||||
|
exit={{
|
||||||
|
opacity: 0,
|
||||||
|
}}
|
||||||
|
{...props}
|
||||||
|
className={actionsheetBackdropStyle({
|
||||||
|
class: className,
|
||||||
|
})}
|
||||||
|
ref={ref}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
const ActionsheetScrollView = React.forwardRef<
|
||||||
|
React.ComponentRef<typeof UIActionsheet.ScrollView>,
|
||||||
|
IActionsheetScrollViewProps
|
||||||
|
>(function ActionsheetScrollView({ className, ...props }, ref) {
|
||||||
|
return (
|
||||||
|
<UIActionsheet.ScrollView
|
||||||
|
className={actionsheetScrollViewStyle({
|
||||||
|
class: className,
|
||||||
|
})}
|
||||||
|
ref={ref}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
const ActionsheetVirtualizedList = React.forwardRef<
|
||||||
|
React.ComponentRef<typeof UIActionsheet.VirtualizedList>,
|
||||||
|
IActionsheetVirtualizedListProps
|
||||||
|
>(function ActionsheetVirtualizedList({ className, ...props }, ref) {
|
||||||
|
return (
|
||||||
|
<UIActionsheet.VirtualizedList
|
||||||
|
className={actionsheetVirtualizedListStyle({
|
||||||
|
class: className,
|
||||||
|
})}
|
||||||
|
ref={ref}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
const ActionsheetFlatList = React.forwardRef<
|
||||||
|
React.ComponentRef<typeof UIActionsheet.FlatList>,
|
||||||
|
IActionsheetFlatListProps
|
||||||
|
>(function ActionsheetFlatList({ className, ...props }, ref) {
|
||||||
|
return (
|
||||||
|
<UIActionsheet.FlatList
|
||||||
|
className={actionsheetFlatListStyle({
|
||||||
|
class: className,
|
||||||
|
})}
|
||||||
|
ref={ref}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
const ActionsheetSectionList = React.forwardRef<
|
||||||
|
React.ComponentRef<typeof UIActionsheet.SectionList>,
|
||||||
|
IActionsheetSectionListProps
|
||||||
|
>(function ActionsheetSectionList({ className, ...props }, ref) {
|
||||||
|
return (
|
||||||
|
<UIActionsheet.SectionList
|
||||||
|
className={actionsheetSectionListStyle({
|
||||||
|
class: className,
|
||||||
|
})}
|
||||||
|
ref={ref}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
const ActionsheetSectionHeaderText = React.forwardRef<
|
||||||
|
React.ComponentRef<typeof UIActionsheet.SectionHeaderText>,
|
||||||
|
IActionsheetSectionHeaderTextProps
|
||||||
|
>(function ActionsheetSectionHeaderText(
|
||||||
|
{
|
||||||
|
className,
|
||||||
|
isTruncated,
|
||||||
|
bold,
|
||||||
|
underline,
|
||||||
|
strikeThrough,
|
||||||
|
size,
|
||||||
|
sub,
|
||||||
|
italic,
|
||||||
|
highlight,
|
||||||
|
...props
|
||||||
|
},
|
||||||
|
ref
|
||||||
|
) {
|
||||||
|
return (
|
||||||
|
<UIActionsheet.SectionHeaderText
|
||||||
|
className={actionsheetSectionHeaderTextStyle({
|
||||||
|
class: className,
|
||||||
|
isTruncated: isTruncated as boolean,
|
||||||
|
bold: bold as boolean,
|
||||||
|
underline: underline as boolean,
|
||||||
|
strikeThrough: strikeThrough as boolean,
|
||||||
|
size,
|
||||||
|
sub: sub as boolean,
|
||||||
|
italic: italic as boolean,
|
||||||
|
highlight: highlight as boolean,
|
||||||
|
})}
|
||||||
|
ref={ref}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
const ActionsheetIcon = React.forwardRef<
|
||||||
|
React.ComponentRef<typeof UIActionsheet.Icon>,
|
||||||
|
IActionsheetIconProps
|
||||||
|
>(function ActionsheetIcon(
|
||||||
|
{ className, as: AsComp, size = 'sm', ...props },
|
||||||
|
ref
|
||||||
|
) {
|
||||||
|
if (AsComp) {
|
||||||
|
return (
|
||||||
|
<AsComp
|
||||||
|
className={actionsheetIconStyle({
|
||||||
|
class: className,
|
||||||
|
size,
|
||||||
|
})}
|
||||||
|
ref={ref}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<UIActionsheet.Icon
|
||||||
|
className={actionsheetIconStyle({
|
||||||
|
class: className,
|
||||||
|
size,
|
||||||
|
})}
|
||||||
|
ref={ref}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
export {
|
||||||
|
Actionsheet,
|
||||||
|
ActionsheetContent,
|
||||||
|
ActionsheetItem,
|
||||||
|
ActionsheetItemText,
|
||||||
|
ActionsheetDragIndicator,
|
||||||
|
ActionsheetDragIndicatorWrapper,
|
||||||
|
ActionsheetBackdrop,
|
||||||
|
ActionsheetScrollView,
|
||||||
|
ActionsheetVirtualizedList,
|
||||||
|
ActionsheetFlatList,
|
||||||
|
ActionsheetSectionList,
|
||||||
|
ActionsheetSectionHeaderText,
|
||||||
|
ActionsheetIcon,
|
||||||
|
};
|
||||||
@ -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