Compare commits

...

2 Commits

Author SHA1 Message Date
a5533e914d Question page realesed 2026-01-05 20:36:43 +01:00
626345bc9d Added answers and question updated 2026-01-05 17:00:28 +01:00
28 changed files with 1444 additions and 101 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,38 +1,18 @@
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}>
<AuthProvider>
<ToastProvider>
<ThemeProvider
value={colorScheme === "dark" ? DarkTheme : DefaultTheme}
>
<Stack> <Stack>
<Stack.Screen name="(tabs)" options={{ headerShown: false }} /> <Stack.Screen name="(tabs)" options={{ headerShown: false }} />
<Stack.Screen <Stack.Screen
@ -41,10 +21,6 @@ export default function RootLayout() {
/> />
</Stack> </Stack>
<StatusBar style="auto" /> <StatusBar style="auto" />
</ThemeProvider> </IndexProvider>
</ToastProvider>
</AuthProvider>
</QueryClientProvider>
</GluestackUIProvider>
); );
} }

View File

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

View File

@ -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} /> }
<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> </Content>
</> </>
) );
} };
export default QuestionScreen; export default QuestionScreen;

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

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

View File

@ -0,0 +1,6 @@
import { useContext } from "react";
import TaxonomyContext from "./context";
const useTaxonomyContext = () => useContext(TaxonomyContext);
export default useTaxonomyContext;

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

View File

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

View File

@ -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: {

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

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

View File

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

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

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

View File

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

View File

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

View File

@ -0,0 +1,6 @@
function capitalizeFirst(str: string) {
if (!str) return '';
return str.charAt(0).toUpperCase() + str.slice(1).toLowerCase();
}
export default capitalizeFirst