Auth and question
This commit is contained in:
parent
7386f012aa
commit
8f8bc0f541
@ -1,13 +1,15 @@
|
|||||||
|
import { deleteAuthToken, getAuthToken } from "@/utils/token-storage";
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
|
|
||||||
|
|
||||||
const axiosInstance = axios.create({
|
const axiosInstance = axios.create({
|
||||||
baseURL: process.env.EXPO_PUBLIC_BACKEND_BASE_URL,
|
baseURL: process.env.EXPO_PUBLIC_BACKEND_BASE_URL,
|
||||||
timeout: 30 * 1000
|
timeout: 30 * 1000
|
||||||
});
|
});
|
||||||
|
|
||||||
axiosInstance.interceptors.request.use(
|
axiosInstance.interceptors.request.use(
|
||||||
(config) => {
|
async config => {
|
||||||
const token = localStorage.getItem("access_token");
|
const token = await getAuthToken();
|
||||||
if (token) config.headers.Authorization = `Bearer ${token}`;
|
if (token) config.headers.Authorization = `Bearer ${token}`;
|
||||||
|
|
||||||
return config;
|
return config;
|
||||||
@ -16,7 +18,7 @@ axiosInstance.interceptors.request.use(
|
|||||||
);
|
);
|
||||||
axiosInstance.interceptors.response.use(undefined, async (error) => {
|
axiosInstance.interceptors.response.use(undefined, async (error) => {
|
||||||
if (error.response?.status === 401) {
|
if (error.response?.status === 401) {
|
||||||
localStorage.removeItem("access_token");
|
await deleteAuthToken();
|
||||||
}
|
}
|
||||||
|
|
||||||
throw error;
|
throw error;
|
||||||
39
api/_client.ts
Normal file
39
api/_client.ts
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
import axiosInstance from "./_axiosInstance";
|
||||||
|
import { AuthLoginResponse, Pagination, QuestionResponse, UserResponse } from "./types";
|
||||||
|
|
||||||
|
export const get_questions = async (page: number, test_id?: number, question_id?: number) => {
|
||||||
|
const response = await axiosInstance.get<Pagination<QuestionResponse>>("/api/questions/", {
|
||||||
|
params: {
|
||||||
|
page,
|
||||||
|
test_id,
|
||||||
|
question_id
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return response.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const get_question = async (id: number) => {
|
||||||
|
const response = await axiosInstance.get<{
|
||||||
|
data: QuestionResponse
|
||||||
|
}>(`/api/questions/${id}`);
|
||||||
|
|
||||||
|
return response.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const get_current_user = async () => {
|
||||||
|
const response = await axiosInstance.get<{
|
||||||
|
user: UserResponse
|
||||||
|
}>("/api/auth/me/");
|
||||||
|
|
||||||
|
return response.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const post_login = async (email: string, password: string) => {
|
||||||
|
const response = await axiosInstance.post<AuthLoginResponse>("/api/auth/login/", {
|
||||||
|
email,
|
||||||
|
password
|
||||||
|
});
|
||||||
|
|
||||||
|
return response.data;
|
||||||
|
}
|
||||||
34
api/auth.ts
Normal file
34
api/auth.ts
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
import { setAuthToken } from "@/utils/token-storage"
|
||||||
|
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"
|
||||||
|
import { AxiosError } from "axios"
|
||||||
|
import { get_current_user, post_login } from "./_client"
|
||||||
|
import { UserResponse } from "./types"
|
||||||
|
|
||||||
|
export const useCurrentUser = () => {
|
||||||
|
return useQuery<UserResponse>({
|
||||||
|
queryKey: ["current-user"],
|
||||||
|
queryFn: async () => {
|
||||||
|
const result = await get_current_user();
|
||||||
|
return result.user;
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
interface LoginFuncAttrs {
|
||||||
|
email: string,
|
||||||
|
password: string
|
||||||
|
}
|
||||||
|
export const useLoginMutation = () => {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
|
return useMutation<any, AxiosError, LoginFuncAttrs>({
|
||||||
|
mutationFn: ({ email, password }: LoginFuncAttrs) => post_login(email, password),
|
||||||
|
onSuccess: async (data) => {
|
||||||
|
await setAuthToken(data.access_token);
|
||||||
|
|
||||||
|
queryClient.invalidateQueries({
|
||||||
|
queryKey: ["current-user"]
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
@ -1,14 +0,0 @@
|
|||||||
import axiosInstance from "./axiosInstance";
|
|
||||||
import { Pagination, QuestionResponse } from "./types";
|
|
||||||
|
|
||||||
export const get_questions = async (page: number, test_id?: number, question_id?: number) => {
|
|
||||||
const response = await axiosInstance.get<Pagination<QuestionResponse>>("/api/questions/", {
|
|
||||||
params: {
|
|
||||||
page,
|
|
||||||
test_id,
|
|
||||||
question_id
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
return response.data;
|
|
||||||
}
|
|
||||||
@ -1,5 +1,5 @@
|
|||||||
import { useQuery } from "@tanstack/react-query";
|
import { useQuery } from "@tanstack/react-query";
|
||||||
import { get_questions } from "./client";
|
import { get_question, get_questions } from "./_client";
|
||||||
|
|
||||||
interface useQuestionsAttr {
|
interface useQuestionsAttr {
|
||||||
page?: number;
|
page?: number;
|
||||||
@ -13,3 +13,13 @@ export const useQuestions = ({ page = 1, test_id, question_id }: useQuestionsAtt
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export const useQuestion = (id: number) => {
|
||||||
|
return useQuery({
|
||||||
|
queryKey: ['question', id],
|
||||||
|
queryFn: async () => {
|
||||||
|
const res = await get_question(id);
|
||||||
|
return res.data;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
@ -65,3 +65,8 @@ export interface UserResponse {
|
|||||||
created_at: string;
|
created_at: string;
|
||||||
updated_at: string;
|
updated_at: string;
|
||||||
}
|
}
|
||||||
|
export interface AuthLoginResponse {
|
||||||
|
access_token: string;
|
||||||
|
token_type: string;
|
||||||
|
user: UserResponse;
|
||||||
|
}
|
||||||
3
app.json
3
app.json
@ -38,7 +38,8 @@
|
|||||||
"backgroundColor": "#000000"
|
"backgroundColor": "#000000"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
],
|
||||||
|
"expo-secure-store"
|
||||||
],
|
],
|
||||||
"experiments": {
|
"experiments": {
|
||||||
"typedRoutes": true,
|
"typedRoutes": true,
|
||||||
|
|||||||
@ -6,10 +6,15 @@ import { HelloWave } from "@/components/hello-wave";
|
|||||||
import ParallaxScrollView from "@/components/parallax-scroll-view";
|
import ParallaxScrollView from "@/components/parallax-scroll-view";
|
||||||
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";
|
||||||
|
|
||||||
export default function HomeScreen() {
|
export default function HomeScreen() {
|
||||||
const { data: questions, isLoading: isLoadingQuestions } = useQuestions();
|
const { data: questions, isLoading: isLoadingQuestions } = useQuestions();
|
||||||
|
const { user, isAuthorized } = useAuthContext();
|
||||||
|
|
||||||
|
const username = isAuthorized ? user!.username : "Guest";
|
||||||
|
|
||||||
const questionsLoaded =
|
const questionsLoaded =
|
||||||
!isLoadingQuestions && questions && questions.meta.total > 0;
|
!isLoadingQuestions && questions && questions.meta.total > 0;
|
||||||
@ -25,13 +30,18 @@ export default function HomeScreen() {
|
|||||||
}
|
}
|
||||||
>
|
>
|
||||||
<ThemedView style={styles.titleContainer}>
|
<ThemedView style={styles.titleContainer}>
|
||||||
<ThemedText type="title">Questions!</ThemedText>
|
<ThemedText type="title">Welcome, {username}!</ThemedText>
|
||||||
<HelloWave />
|
<HelloWave />
|
||||||
</ThemedView>
|
</ThemedView>
|
||||||
|
|
||||||
|
<ThemedText type="subtitle">Questions</ThemedText>
|
||||||
{questionsLoaded &&
|
{questionsLoaded &&
|
||||||
questions.data.map((question) => (
|
questions.data.map((question) => (
|
||||||
<Question key={question.id} question={question} />
|
<Question
|
||||||
|
key={question.id}
|
||||||
|
question={question}
|
||||||
|
onPress={() => router.push(`/questions/${question.id}`)}
|
||||||
|
/>
|
||||||
))}
|
))}
|
||||||
</ParallaxScrollView>
|
</ParallaxScrollView>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -1,57 +1,20 @@
|
|||||||
import { Image } from 'expo-image';
|
|
||||||
import { Platform, StyleSheet } from 'react-native';
|
|
||||||
|
|
||||||
import ParallaxScrollView from '@/components/parallax-scroll-view';
|
import useAuthContext from '@/components/ui/auth-provider/hook';
|
||||||
import { ThemedView } from '@/components/themed-view';
|
import { Button, ButtonText } from '@/components/ui/button';
|
||||||
import { ThemedText } from '@/components/ui/themed-text';
|
import Content from '@/components/ui/content';
|
||||||
|
import { Divider } from '@/components/ui/divider';
|
||||||
|
import UserHeader from '@/components/user-header';
|
||||||
|
|
||||||
|
export default function MeScreen() {
|
||||||
|
const { user, logout, isAuthorized } = useAuthContext();
|
||||||
|
|
||||||
export default function HomeScreen() {
|
|
||||||
return (
|
return (
|
||||||
<ParallaxScrollView
|
<Content>
|
||||||
headerBackgroundColor={{ light: '#A1CEDC', dark: '#1D3D47' }}
|
<UserHeader user={user} />
|
||||||
headerImage={
|
|
||||||
<Image
|
<Divider className='mt-3 mb-3' />
|
||||||
source={require('@/assets/images/partial-react-logo.png')}
|
|
||||||
style={styles.reactLogo}
|
{isAuthorized && <Button action="negative" onPress={() => logout()}><ButtonText>Logout</ButtonText></Button>}
|
||||||
/>
|
</Content>
|
||||||
}>
|
|
||||||
<ThemedView style={styles.titleContainer}>
|
|
||||||
<ThemedText type="title">Welcome!</ThemedText>
|
|
||||||
</ThemedView>
|
|
||||||
<ThemedView style={styles.stepContainer}>
|
|
||||||
<ThemedText type="subtitle">Step 1: Try it</ThemedText>
|
|
||||||
<ThemedText>
|
|
||||||
Edit <ThemedText type="defaultSemiBold">app/(tabs)/index.tsx</ThemedText> to see changes.
|
|
||||||
Press{' '}
|
|
||||||
<ThemedText type="defaultSemiBold">
|
|
||||||
{Platform.select({
|
|
||||||
ios: 'cmd + d',
|
|
||||||
android: 'cmd + m',
|
|
||||||
web: 'F12',
|
|
||||||
})}
|
|
||||||
</ThemedText>{' '}
|
|
||||||
to open developer tools.
|
|
||||||
</ThemedText>
|
|
||||||
</ThemedView>
|
|
||||||
</ParallaxScrollView>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
|
||||||
titleContainer: {
|
|
||||||
flexDirection: 'row',
|
|
||||||
alignItems: 'center',
|
|
||||||
gap: 8,
|
|
||||||
},
|
|
||||||
stepContainer: {
|
|
||||||
gap: 8,
|
|
||||||
marginBottom: 8,
|
|
||||||
},
|
|
||||||
reactLogo: {
|
|
||||||
height: 178,
|
|
||||||
width: 290,
|
|
||||||
bottom: 0,
|
|
||||||
left: 0,
|
|
||||||
position: 'absolute',
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|||||||
@ -1,112 +1,32 @@
|
|||||||
import { Image } from 'expo-image';
|
import { View } from 'react-native';
|
||||||
import { Platform, StyleSheet } from 'react-native';
|
|
||||||
|
|
||||||
import { ExternalLink } from '@/components/external-link';
|
import { useQuestions } from '@/api';
|
||||||
import ParallaxScrollView from '@/components/parallax-scroll-view';
|
import Question from '@/components/question';
|
||||||
import { ThemedView } from '@/components/themed-view';
|
import Content from '@/components/ui/content';
|
||||||
import { Collapsible } from '@/components/ui/collapsible';
|
|
||||||
import { IconSymbol } from '@/components/ui/icon-symbol';
|
|
||||||
import { ThemedText } from '@/components/ui/themed-text';
|
import { ThemedText } from '@/components/ui/themed-text';
|
||||||
import { Fonts } from '@/constants/theme';
|
import { router } from 'expo-router';
|
||||||
|
|
||||||
|
export default function QuestionsScreen() {
|
||||||
|
const { data: questions, isLoading: isLoadingQuestions } = useQuestions();
|
||||||
|
|
||||||
|
const questionsLoaded =
|
||||||
|
!isLoadingQuestions && questions && questions.meta.total > 0;
|
||||||
|
|
||||||
export default function TabTwoScreen() {
|
|
||||||
return (
|
return (
|
||||||
<ParallaxScrollView
|
<Content>
|
||||||
headerBackgroundColor={{ light: '#D0D0D0', dark: '#353636' }}
|
<ThemedText type="title" className='mb-3'>Questions</ThemedText>
|
||||||
headerImage={
|
|
||||||
<IconSymbol
|
|
||||||
size={310}
|
|
||||||
color="#808080"
|
|
||||||
name="chevron.left.forwardslash.chevron.right"
|
|
||||||
style={styles.headerImage}
|
|
||||||
/>
|
|
||||||
}>
|
|
||||||
<ThemedView style={styles.titleContainer}>
|
|
||||||
<ThemedText
|
|
||||||
type="title"
|
|
||||||
style={{
|
|
||||||
fontFamily: Fonts.rounded,
|
|
||||||
}}>
|
|
||||||
Explore
|
|
||||||
</ThemedText>
|
|
||||||
</ThemedView>
|
|
||||||
<ThemedText>This app includes example code to help you get started.</ThemedText>
|
|
||||||
<Collapsible title="File-based routing">
|
|
||||||
<ThemedText>
|
|
||||||
This app has two screens:{' '}
|
|
||||||
<ThemedText type="defaultSemiBold">app/(tabs)/index.tsx</ThemedText> and{' '}
|
|
||||||
<ThemedText type="defaultSemiBold">app/(tabs)/explore.tsx</ThemedText>
|
|
||||||
</ThemedText>
|
|
||||||
<ThemedText>
|
|
||||||
The layout file in <ThemedText type="defaultSemiBold">app/(tabs)/_layout.tsx</ThemedText>{' '}
|
|
||||||
sets up the tab navigator.
|
|
||||||
</ThemedText>
|
|
||||||
<ExternalLink href="https://docs.expo.dev/router/introduction">
|
|
||||||
<ThemedText type="link">Learn more</ThemedText>
|
|
||||||
</ExternalLink>
|
|
||||||
</Collapsible>
|
|
||||||
<Collapsible title="Android, iOS, and web support">
|
|
||||||
<ThemedText>
|
|
||||||
You can open this project on Android, iOS, and the web. To open the web version, press{' '}
|
|
||||||
<ThemedText type="defaultSemiBold">w</ThemedText> in the terminal running this project.
|
|
||||||
</ThemedText>
|
|
||||||
</Collapsible>
|
|
||||||
<Collapsible title="Images">
|
|
||||||
<ThemedText>
|
|
||||||
For static images, you can use the <ThemedText type="defaultSemiBold">@2x</ThemedText> and{' '}
|
|
||||||
<ThemedText type="defaultSemiBold">@3x</ThemedText> suffixes to provide files for
|
|
||||||
different screen densities
|
|
||||||
</ThemedText>
|
|
||||||
<Image
|
|
||||||
source={require('@/assets/images/react-logo.png')}
|
|
||||||
style={{ width: 100, height: 100, alignSelf: 'center' }}
|
|
||||||
/>
|
|
||||||
<ExternalLink href="https://reactnative.dev/docs/images">
|
|
||||||
<ThemedText type="link">Learn more</ThemedText>
|
|
||||||
</ExternalLink>
|
|
||||||
</Collapsible>
|
|
||||||
<Collapsible title="Light and dark mode components">
|
|
||||||
<ThemedText>
|
|
||||||
This template has light and dark mode support. The{' '}
|
|
||||||
<ThemedText type="defaultSemiBold">useColorScheme()</ThemedText> hook lets you inspect
|
|
||||||
what the user's current color scheme is, and so you can adjust UI colors accordingly.
|
|
||||||
</ThemedText>
|
|
||||||
<ExternalLink href="https://docs.expo.dev/develop/user-interface/color-themes/">
|
|
||||||
<ThemedText type="link">Learn more</ThemedText>
|
|
||||||
</ExternalLink>
|
|
||||||
</Collapsible>
|
|
||||||
<Collapsible title="Animations">
|
|
||||||
<ThemedText>
|
|
||||||
This template includes an example of an animated component. The{' '}
|
|
||||||
<ThemedText type="defaultSemiBold">components/HelloWave.tsx</ThemedText> component uses
|
|
||||||
the powerful{' '}
|
|
||||||
<ThemedText type="defaultSemiBold" style={{ fontFamily: Fonts.mono }}>
|
|
||||||
react-native-reanimated
|
|
||||||
</ThemedText>{' '}
|
|
||||||
library to create a waving hand animation.
|
|
||||||
</ThemedText>
|
|
||||||
{Platform.select({
|
|
||||||
ios: (
|
|
||||||
<ThemedText>
|
|
||||||
The <ThemedText type="defaultSemiBold">components/ParallaxScrollView.tsx</ThemedText>{' '}
|
|
||||||
component provides a parallax effect for the header image.
|
|
||||||
</ThemedText>
|
|
||||||
),
|
|
||||||
})}
|
|
||||||
</Collapsible>
|
|
||||||
</ParallaxScrollView>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
<View className='gap-3'>
|
||||||
headerImage: {
|
{questionsLoaded &&
|
||||||
color: '#808080',
|
questions.data.map((question) => (
|
||||||
bottom: -90,
|
<Question
|
||||||
left: -35,
|
key={question.id}
|
||||||
position: 'absolute',
|
question={question}
|
||||||
},
|
onPress={() => router.push(`/questions/${question.id}`)}
|
||||||
titleContainer: {
|
/>
|
||||||
flexDirection: 'row',
|
))}
|
||||||
gap: 8,
|
</View>
|
||||||
},
|
|
||||||
});
|
</Content>
|
||||||
|
)
|
||||||
|
}
|
||||||
@ -1,57 +1,11 @@
|
|||||||
import { Image } from 'expo-image';
|
|
||||||
import { Platform, StyleSheet } from 'react-native';
|
|
||||||
|
|
||||||
import ParallaxScrollView from '@/components/parallax-scroll-view';
|
import Content from '@/components/ui/content';
|
||||||
import { ThemedView } from '@/components/themed-view';
|
|
||||||
import { ThemedText } from '@/components/ui/themed-text';
|
import { ThemedText } from '@/components/ui/themed-text';
|
||||||
|
|
||||||
export default function HomeScreen() {
|
export default function TestsScreen() {
|
||||||
return (
|
return (
|
||||||
<ParallaxScrollView
|
<Content>
|
||||||
headerBackgroundColor={{ light: '#A1CEDC', dark: '#1D3D47' }}
|
<ThemedText type="title" className='mb-3'>Tests</ThemedText>
|
||||||
headerImage={
|
</Content>
|
||||||
<Image
|
|
||||||
source={require('@/assets/images/partial-react-logo.png')}
|
|
||||||
style={styles.reactLogo}
|
|
||||||
/>
|
|
||||||
}>
|
|
||||||
<ThemedView style={styles.titleContainer}>
|
|
||||||
<ThemedText type="title">Welcome!</ThemedText>
|
|
||||||
</ThemedView>
|
|
||||||
<ThemedView style={styles.stepContainer}>
|
|
||||||
<ThemedText type="subtitle">Step 1: Try it</ThemedText>
|
|
||||||
<ThemedText>
|
|
||||||
Edit <ThemedText type="defaultSemiBold">app/(tabs)/index.tsx</ThemedText> to see changes.
|
|
||||||
Press{' '}
|
|
||||||
<ThemedText type="defaultSemiBold">
|
|
||||||
{Platform.select({
|
|
||||||
ios: 'cmd + d',
|
|
||||||
android: 'cmd + m',
|
|
||||||
web: 'F12',
|
|
||||||
})}
|
|
||||||
</ThemedText>{' '}
|
|
||||||
to open developer tools.
|
|
||||||
</ThemedText>
|
|
||||||
</ThemedView>
|
|
||||||
</ParallaxScrollView>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
|
||||||
titleContainer: {
|
|
||||||
flexDirection: 'row',
|
|
||||||
alignItems: 'center',
|
|
||||||
gap: 8,
|
|
||||||
},
|
|
||||||
stepContainer: {
|
|
||||||
gap: 8,
|
|
||||||
marginBottom: 8,
|
|
||||||
},
|
|
||||||
reactLogo: {
|
|
||||||
height: 178,
|
|
||||||
width: 290,
|
|
||||||
bottom: 0,
|
|
||||||
left: 0,
|
|
||||||
position: 'absolute',
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|||||||
@ -1,29 +1,50 @@
|
|||||||
import { DarkTheme, DefaultTheme, ThemeProvider } from '@react-navigation/native';
|
import {
|
||||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
DarkTheme,
|
||||||
import { Stack } from 'expo-router';
|
DefaultTheme,
|
||||||
import { StatusBar } from 'expo-status-bar';
|
ThemeProvider,
|
||||||
import 'react-native-reanimated';
|
} from "@react-navigation/native";
|
||||||
|
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||||
|
import { Stack } from "expo-router";
|
||||||
|
import { StatusBar } from "expo-status-bar";
|
||||||
|
import "react-native-reanimated";
|
||||||
|
|
||||||
import { useColorScheme } from '@/hooks/use-color-scheme';
|
import { useColorScheme } from "@/hooks/use-color-scheme";
|
||||||
|
|
||||||
|
import AuthProvider from "@/components/ui/auth-provider";
|
||||||
|
import { GluestackUIProvider } from "@/components/ui/gluestack-ui-provider";
|
||||||
|
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();
|
const queryClient = new QueryClient();
|
||||||
|
|
||||||
export default function RootLayout() {
|
export default function RootLayout() {
|
||||||
const colorScheme = useColorScheme();
|
const colorScheme = useColorScheme();
|
||||||
|
const mode = colorScheme === "dark" ? "dark" : "light";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<GluestackUIProvider mode={mode}>
|
||||||
<QueryClientProvider client={queryClient}>
|
<QueryClientProvider client={queryClient}>
|
||||||
<ThemeProvider value={colorScheme === 'dark' ? DarkTheme : DefaultTheme}>
|
<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 name="modal" options={{ presentation: 'modal', title: 'Modal' }} />
|
<Stack.Screen
|
||||||
|
name="modal"
|
||||||
|
options={{ presentation: "modal", title: "Modal" }}
|
||||||
|
/>
|
||||||
</Stack>
|
</Stack>
|
||||||
<StatusBar style="auto" />
|
<StatusBar style="auto" />
|
||||||
</ThemeProvider>
|
</ThemeProvider>
|
||||||
|
</ToastProvider>
|
||||||
|
</AuthProvider>
|
||||||
</QueryClientProvider>
|
</QueryClientProvider>
|
||||||
|
</GluestackUIProvider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
39
app/login.tsx
Normal file
39
app/login.tsx
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
import useAuthContext from "@/components/ui/auth-provider/hook";
|
||||||
|
import Content from "@/components/ui/content";
|
||||||
|
import LoginForm, { LoginFormData } from "@/components/ui/login-form";
|
||||||
|
import Panel from "@/components/ui/panel";
|
||||||
|
import { ThemedText } from "@/components/ui/themed-text";
|
||||||
|
import getErrorAxiosMessage from "@/utils/get-error-axios-message";
|
||||||
|
import { Stack } from "expo-router";
|
||||||
|
import { useToast } from "react-native-toast-notifications";
|
||||||
|
|
||||||
|
const LoginScreen = () => {
|
||||||
|
const { login, isPendingLogin } = useAuthContext();
|
||||||
|
const toast = useToast();
|
||||||
|
|
||||||
|
const onLoginSubmit = (data: LoginFormData) => {
|
||||||
|
login(data.email, data.password, {
|
||||||
|
onSuccess: (data) => {
|
||||||
|
toast.show("Hello, " + data.user.username, { type: "success" });
|
||||||
|
},
|
||||||
|
onError: (error) => {
|
||||||
|
toast.show(getErrorAxiosMessage(error), { type: "danger" });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Stack.Screen options={{ title: "Sign in" }} />
|
||||||
|
<Content>
|
||||||
|
<Panel>
|
||||||
|
<ThemedText type="subtitle" className="text-center mb-2">Login</ThemedText>
|
||||||
|
<ThemedText className="text-center mb-2">Hello there</ThemedText>
|
||||||
|
<LoginForm onSubmit={onLoginSubmit} isLoading={isPendingLogin} />
|
||||||
|
</Panel>
|
||||||
|
</Content>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default LoginScreen;
|
||||||
23
app/questions/[id].tsx
Normal file
23
app/questions/[id].tsx
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
import { useQuestion } from "@/api";
|
||||||
|
import Question from "@/components/question";
|
||||||
|
import Content from "@/components/ui/content";
|
||||||
|
import { Skeleton } from "@/components/ui/skeleton";
|
||||||
|
import { Stack, useLocalSearchParams } from "expo-router";
|
||||||
|
|
||||||
|
const QuestionScreen = () => {
|
||||||
|
const { id: idParam } = useLocalSearchParams<{ id: string }>();
|
||||||
|
const id = +idParam;
|
||||||
|
const { data, isLoading } = useQuestion(id);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Stack.Screen options={{ title: `Question #${id}` }} />
|
||||||
|
<Content>
|
||||||
|
{isLoading && <Skeleton variant="rounded" className="h-32" />}
|
||||||
|
{ data && <Question question={data} /> }
|
||||||
|
</Content>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default QuestionScreen;
|
||||||
22
babel.config.js
Normal file
22
babel.config.js
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
module.exports = function (api) {
|
||||||
|
api.cache(true);
|
||||||
|
|
||||||
|
return {
|
||||||
|
presets: [['babel-preset-expo'], 'nativewind/babel'],
|
||||||
|
|
||||||
|
plugins: [
|
||||||
|
[
|
||||||
|
'module-resolver',
|
||||||
|
{
|
||||||
|
root: ['./'],
|
||||||
|
|
||||||
|
alias: {
|
||||||
|
'@': './',
|
||||||
|
'tailwind.config': './tailwind.config.js',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
'react-native-worklets/plugin',
|
||||||
|
],
|
||||||
|
};
|
||||||
|
};
|
||||||
@ -8,6 +8,7 @@ import Animated, {
|
|||||||
} from 'react-native-reanimated';
|
} from 'react-native-reanimated';
|
||||||
|
|
||||||
import { ThemedView } from '@/components/themed-view';
|
import { ThemedView } from '@/components/themed-view';
|
||||||
|
import { ContentPadding } from '@/constants/theme';
|
||||||
import { useColorScheme } from '@/hooks/use-color-scheme';
|
import { useColorScheme } from '@/hooks/use-color-scheme';
|
||||||
import { useThemeColor } from '@/hooks/use-theme-color';
|
import { useThemeColor } from '@/hooks/use-theme-color';
|
||||||
|
|
||||||
@ -72,7 +73,7 @@ const styles = StyleSheet.create({
|
|||||||
},
|
},
|
||||||
content: {
|
content: {
|
||||||
flex: 1,
|
flex: 1,
|
||||||
padding: 32,
|
padding: ContentPadding,
|
||||||
gap: 16,
|
gap: 16,
|
||||||
overflow: 'hidden',
|
overflow: 'hidden',
|
||||||
},
|
},
|
||||||
|
|||||||
@ -8,7 +8,7 @@ export type ThemedViewProps = ViewProps & {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export function ThemedView({ style, lightColor, darkColor, ...otherProps }: ThemedViewProps) {
|
export function ThemedView({ style, lightColor, darkColor, ...otherProps }: ThemedViewProps) {
|
||||||
const backgroundColor = useThemeColor({ light: lightColor, dark: darkColor }, 'background');
|
const backgroundColor = useThemeColor({ light: lightColor, dark: darkColor }, 'defaultBackground');
|
||||||
|
|
||||||
return <View style={[{ backgroundColor }, style]} {...otherProps} />;
|
return <View style={[{ backgroundColor }, style]} {...otherProps} />;
|
||||||
}
|
}
|
||||||
|
|||||||
30
components/ui/auth-provider/context.tsx
Normal file
30
components/ui/auth-provider/context.tsx
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
import { AuthLoginResponse, UserResponse } from "@/api/types";
|
||||||
|
import { AxiosError } from "axios";
|
||||||
|
import { createContext } from "react";
|
||||||
|
|
||||||
|
export interface LoginOptions {
|
||||||
|
onSuccess?: (data: AuthLoginResponse) => void;
|
||||||
|
onError?: (error: AxiosError) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
interface AuthContextType {
|
||||||
|
user?: UserResponse;
|
||||||
|
isAuthorized: boolean;
|
||||||
|
isLoading: boolean;
|
||||||
|
login: (email: string, password: string, options?: LoginOptions) => void;
|
||||||
|
logout: () => void;
|
||||||
|
errorLogin?: Error|null;
|
||||||
|
isPendingLogin: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const AuthContext = createContext<AuthContextType>({
|
||||||
|
user: undefined,
|
||||||
|
isAuthorized: false,
|
||||||
|
isLoading: false,
|
||||||
|
login: (email, password) => {},
|
||||||
|
logout: () => {},
|
||||||
|
errorLogin: null,
|
||||||
|
isPendingLogin: false
|
||||||
|
});
|
||||||
|
|
||||||
|
export default AuthContext;
|
||||||
6
components/ui/auth-provider/hook.ts
Normal file
6
components/ui/auth-provider/hook.ts
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
import { useContext } from "react";
|
||||||
|
import AuthContext from "./context";
|
||||||
|
|
||||||
|
const useAuthContext = () => useContext(AuthContext);
|
||||||
|
|
||||||
|
export default useAuthContext;
|
||||||
80
components/ui/auth-provider/index.tsx
Normal file
80
components/ui/auth-provider/index.tsx
Normal file
@ -0,0 +1,80 @@
|
|||||||
|
import { useCurrentUser, useLoginMutation } from "@/api/auth";
|
||||||
|
import { deleteAuthToken, getAuthToken } from "@/utils/token-storage";
|
||||||
|
import { useQueryClient } from "@tanstack/react-query";
|
||||||
|
import { router } from "expo-router";
|
||||||
|
import { ReactNode, useEffect, useState } from "react";
|
||||||
|
import AuthContext, { LoginOptions } from "./context";
|
||||||
|
|
||||||
|
interface AuthProviderProps {
|
||||||
|
children: ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function AuthProvider({ children }: AuthProviderProps) {
|
||||||
|
const [storageStatus, setStorageStatus] = useState<"pending"|"hasKey"|"noKey">("pending");
|
||||||
|
const { data: user, isLoading: isLoadingUser, refetch: refetchUser } = useCurrentUser();
|
||||||
|
const {
|
||||||
|
mutate: mutateLogin,
|
||||||
|
isPending: isPendingLogin,
|
||||||
|
error: errorLogin,
|
||||||
|
} = useLoginMutation();
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const authTokenFunc = async () => {
|
||||||
|
const token = await getAuthToken()
|
||||||
|
if(token)
|
||||||
|
setStorageStatus("hasKey");
|
||||||
|
else
|
||||||
|
setStorageStatus("noKey");
|
||||||
|
}
|
||||||
|
authTokenFunc();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const isAuthorized = !!user;
|
||||||
|
const isLoading = (isLoadingUser || isPendingLogin) && storageStatus === "hasKey";
|
||||||
|
|
||||||
|
const login = (email: string, password: string, options?: LoginOptions) => {
|
||||||
|
mutateLogin({
|
||||||
|
email,
|
||||||
|
password,
|
||||||
|
}, {
|
||||||
|
onSuccess: (data) => {
|
||||||
|
refetchUser();
|
||||||
|
setStorageStatus("hasKey");
|
||||||
|
router.push("/(tabs)/me");
|
||||||
|
|
||||||
|
if(!options)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if(options.onSuccess)
|
||||||
|
options.onSuccess(data);
|
||||||
|
},
|
||||||
|
onError: (error) => {
|
||||||
|
if(!options)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if(options.onError)
|
||||||
|
options.onError(error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
const logout = async () => {
|
||||||
|
await deleteAuthToken();
|
||||||
|
setStorageStatus("noKey");
|
||||||
|
queryClient.invalidateQueries({
|
||||||
|
queryKey: ["current-user"]
|
||||||
|
});
|
||||||
|
queryClient.setQueryData(["current-user"], null);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AuthContext.Provider
|
||||||
|
value={{ user, isLoading, isAuthorized, login, logout, errorLogin, isPendingLogin }}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</AuthContext.Provider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default AuthProvider;
|
||||||
185
components/ui/avatar/index.tsx
Normal file
185
components/ui/avatar/index.tsx
Normal file
@ -0,0 +1,185 @@
|
|||||||
|
'use client';
|
||||||
|
import React from 'react';
|
||||||
|
import { createAvatar } from '@gluestack-ui/core/avatar/creator';
|
||||||
|
|
||||||
|
import { View, Text, Image, Platform } from 'react-native';
|
||||||
|
|
||||||
|
import { tva } from '@gluestack-ui/utils/nativewind-utils';
|
||||||
|
import {
|
||||||
|
withStyleContext,
|
||||||
|
useStyleContext,
|
||||||
|
} from '@gluestack-ui/utils/nativewind-utils';
|
||||||
|
const SCOPE = 'AVATAR';
|
||||||
|
import type { VariantProps } from '@gluestack-ui/utils/nativewind-utils';
|
||||||
|
|
||||||
|
const UIAvatar = createAvatar({
|
||||||
|
Root: withStyleContext(View, SCOPE),
|
||||||
|
Badge: View,
|
||||||
|
Group: View,
|
||||||
|
Image: Image,
|
||||||
|
FallbackText: Text,
|
||||||
|
});
|
||||||
|
|
||||||
|
const avatarStyle = tva({
|
||||||
|
base: 'rounded-full justify-center items-center relative bg-primary-600 group-[.avatar-group]/avatar-group:-ml-2.5',
|
||||||
|
variants: {
|
||||||
|
size: {
|
||||||
|
'xs': 'w-6 h-6',
|
||||||
|
'sm': 'w-8 h-8',
|
||||||
|
'md': 'w-12 h-12',
|
||||||
|
'lg': 'w-16 h-16',
|
||||||
|
'xl': 'w-24 h-24',
|
||||||
|
'2xl': 'w-32 h-32',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const avatarFallbackTextStyle = tva({
|
||||||
|
base: 'text-typography-0 font-semibold overflow-hidden text-transform:uppercase web:cursor-default',
|
||||||
|
|
||||||
|
parentVariants: {
|
||||||
|
size: {
|
||||||
|
'xs': 'text-2xs',
|
||||||
|
'sm': 'text-xs',
|
||||||
|
'md': 'text-base',
|
||||||
|
'lg': 'text-xl',
|
||||||
|
'xl': 'text-3xl',
|
||||||
|
'2xl': 'text-5xl',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const avatarGroupStyle = tva({
|
||||||
|
base: 'group/avatar-group flex-row-reverse relative avatar-group',
|
||||||
|
});
|
||||||
|
|
||||||
|
const avatarBadgeStyle = tva({
|
||||||
|
base: 'w-5 h-5 bg-success-500 rounded-full absolute right-0 bottom-0 border-background-0 border-2',
|
||||||
|
parentVariants: {
|
||||||
|
size: {
|
||||||
|
'xs': 'w-2 h-2',
|
||||||
|
'sm': 'w-2 h-2',
|
||||||
|
'md': 'w-3 h-3',
|
||||||
|
'lg': 'w-4 h-4',
|
||||||
|
'xl': 'w-6 h-6',
|
||||||
|
'2xl': 'w-8 h-8',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const avatarImageStyle = tva({
|
||||||
|
base: 'h-full w-full rounded-full absolute',
|
||||||
|
});
|
||||||
|
|
||||||
|
type IAvatarProps = Omit<
|
||||||
|
React.ComponentPropsWithoutRef<typeof UIAvatar>,
|
||||||
|
'context'
|
||||||
|
> &
|
||||||
|
VariantProps<typeof avatarStyle>;
|
||||||
|
|
||||||
|
const Avatar = React.forwardRef<
|
||||||
|
React.ComponentRef<typeof UIAvatar>,
|
||||||
|
IAvatarProps
|
||||||
|
>(function Avatar({ className, size = 'md', ...props }, ref) {
|
||||||
|
return (
|
||||||
|
<UIAvatar
|
||||||
|
ref={ref}
|
||||||
|
{...props}
|
||||||
|
className={avatarStyle({ size, class: className })}
|
||||||
|
context={{ size }}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
type IAvatarBadgeProps = React.ComponentPropsWithoutRef<typeof UIAvatar.Badge> &
|
||||||
|
VariantProps<typeof avatarBadgeStyle>;
|
||||||
|
|
||||||
|
const AvatarBadge = React.forwardRef<
|
||||||
|
React.ComponentRef<typeof UIAvatar.Badge>,
|
||||||
|
IAvatarBadgeProps
|
||||||
|
>(function AvatarBadge({ className, size, ...props }, ref) {
|
||||||
|
const { size: parentSize } = useStyleContext(SCOPE);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<UIAvatar.Badge
|
||||||
|
ref={ref}
|
||||||
|
{...props}
|
||||||
|
className={avatarBadgeStyle({
|
||||||
|
parentVariants: {
|
||||||
|
size: parentSize,
|
||||||
|
},
|
||||||
|
size,
|
||||||
|
class: className,
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
type IAvatarFallbackTextProps = React.ComponentPropsWithoutRef<
|
||||||
|
typeof UIAvatar.FallbackText
|
||||||
|
> &
|
||||||
|
VariantProps<typeof avatarFallbackTextStyle>;
|
||||||
|
const AvatarFallbackText = React.forwardRef<
|
||||||
|
React.ComponentRef<typeof UIAvatar.FallbackText>,
|
||||||
|
IAvatarFallbackTextProps
|
||||||
|
>(function AvatarFallbackText({ className, size, ...props }, ref) {
|
||||||
|
const { size: parentSize } = useStyleContext(SCOPE);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<UIAvatar.FallbackText
|
||||||
|
ref={ref}
|
||||||
|
{...props}
|
||||||
|
className={avatarFallbackTextStyle({
|
||||||
|
parentVariants: {
|
||||||
|
size: parentSize,
|
||||||
|
},
|
||||||
|
size,
|
||||||
|
class: className,
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
type IAvatarImageProps = React.ComponentPropsWithoutRef<typeof UIAvatar.Image> &
|
||||||
|
VariantProps<typeof avatarImageStyle>;
|
||||||
|
|
||||||
|
const AvatarImage = React.forwardRef<
|
||||||
|
React.ComponentRef<typeof UIAvatar.Image>,
|
||||||
|
IAvatarImageProps
|
||||||
|
>(function AvatarImage({ className, ...props }, ref) {
|
||||||
|
return (
|
||||||
|
<UIAvatar.Image
|
||||||
|
ref={ref}
|
||||||
|
{...props}
|
||||||
|
className={avatarImageStyle({
|
||||||
|
class: className,
|
||||||
|
})}
|
||||||
|
// @ts-expect-error : This is a workaround to fix the issue with the image style on web.
|
||||||
|
style={
|
||||||
|
Platform.OS === 'web'
|
||||||
|
? { height: 'revert-layer', width: 'revert-layer' }
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
type IAvatarGroupProps = React.ComponentPropsWithoutRef<typeof UIAvatar.Group> &
|
||||||
|
VariantProps<typeof avatarGroupStyle>;
|
||||||
|
|
||||||
|
const AvatarGroup = React.forwardRef<
|
||||||
|
React.ComponentRef<typeof UIAvatar.Group>,
|
||||||
|
IAvatarGroupProps
|
||||||
|
>(function AvatarGroup({ className, ...props }, ref) {
|
||||||
|
return (
|
||||||
|
<UIAvatar.Group
|
||||||
|
ref={ref}
|
||||||
|
{...props}
|
||||||
|
className={avatarGroupStyle({
|
||||||
|
class: className,
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
export { Avatar, AvatarBadge, AvatarFallbackText, AvatarImage, AvatarGroup };
|
||||||
434
components/ui/button/index.tsx
Normal file
434
components/ui/button/index.tsx
Normal file
@ -0,0 +1,434 @@
|
|||||||
|
'use client';
|
||||||
|
import React from 'react';
|
||||||
|
import { createButton } from '@gluestack-ui/core/button/creator';
|
||||||
|
import {
|
||||||
|
tva,
|
||||||
|
withStyleContext,
|
||||||
|
useStyleContext,
|
||||||
|
type VariantProps,
|
||||||
|
} from '@gluestack-ui/utils/nativewind-utils';
|
||||||
|
import { cssInterop } from 'nativewind';
|
||||||
|
import { ActivityIndicator, Pressable, Text, View } from 'react-native';
|
||||||
|
import { PrimitiveIcon, UIIcon } from '@gluestack-ui/core/icon/creator';
|
||||||
|
|
||||||
|
const SCOPE = 'BUTTON';
|
||||||
|
|
||||||
|
const Root = withStyleContext(Pressable, SCOPE);
|
||||||
|
|
||||||
|
const UIButton = createButton({
|
||||||
|
Root: Root,
|
||||||
|
Text,
|
||||||
|
Group: View,
|
||||||
|
Spinner: ActivityIndicator,
|
||||||
|
Icon: UIIcon,
|
||||||
|
});
|
||||||
|
|
||||||
|
cssInterop(PrimitiveIcon, {
|
||||||
|
className: {
|
||||||
|
target: 'style',
|
||||||
|
nativeStyleToProp: {
|
||||||
|
height: true,
|
||||||
|
width: true,
|
||||||
|
fill: true,
|
||||||
|
color: 'classNameColor',
|
||||||
|
stroke: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const buttonStyle = tva({
|
||||||
|
base: 'group/button rounded bg-primary-500 flex-row items-center justify-center data-[focus-visible=true]:web:outline-none data-[focus-visible=true]:web:ring-2 data-[disabled=true]:opacity-40 gap-2',
|
||||||
|
variants: {
|
||||||
|
action: {
|
||||||
|
primary:
|
||||||
|
'bg-primary-500 data-[hover=true]:bg-primary-600 data-[active=true]:bg-primary-700 border-primary-300 data-[hover=true]:border-primary-400 data-[active=true]:border-primary-500 data-[focus-visible=true]:web:ring-indicator-info',
|
||||||
|
secondary:
|
||||||
|
'bg-secondary-500 border-secondary-300 data-[hover=true]:bg-secondary-600 data-[hover=true]:border-secondary-400 data-[active=true]:bg-secondary-700 data-[active=true]:border-secondary-700 data-[focus-visible=true]:web:ring-indicator-info',
|
||||||
|
positive:
|
||||||
|
'bg-success-500 border-success-300 data-[hover=true]:bg-success-600 data-[hover=true]:border-success-400 data-[active=true]:bg-success-700 data-[active=true]:border-success-500 data-[focus-visible=true]:web:ring-indicator-info',
|
||||||
|
negative:
|
||||||
|
'bg-error-500 border-error-300 data-[hover=true]:bg-error-600 data-[hover=true]:border-error-400 data-[active=true]:bg-error-700 data-[active=true]:border-error-500 data-[focus-visible=true]:web:ring-indicator-info',
|
||||||
|
default:
|
||||||
|
'bg-transparent data-[hover=true]:bg-background-50 data-[active=true]:bg-transparent',
|
||||||
|
},
|
||||||
|
variant: {
|
||||||
|
link: 'px-0',
|
||||||
|
outline:
|
||||||
|
'bg-transparent border data-[hover=true]:bg-background-50 data-[active=true]:bg-transparent',
|
||||||
|
solid: '',
|
||||||
|
},
|
||||||
|
|
||||||
|
size: {
|
||||||
|
xs: 'px-3.5 h-8',
|
||||||
|
sm: 'px-4 h-9',
|
||||||
|
md: 'px-5 h-10',
|
||||||
|
lg: 'px-6 h-11',
|
||||||
|
xl: 'px-7 h-12',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
compoundVariants: [
|
||||||
|
{
|
||||||
|
action: 'primary',
|
||||||
|
variant: 'link',
|
||||||
|
class:
|
||||||
|
'px-0 bg-transparent data-[hover=true]:bg-transparent data-[active=true]:bg-transparent',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
action: 'secondary',
|
||||||
|
variant: 'link',
|
||||||
|
class:
|
||||||
|
'px-0 bg-transparent data-[hover=true]:bg-transparent data-[active=true]:bg-transparent',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
action: 'positive',
|
||||||
|
variant: 'link',
|
||||||
|
class:
|
||||||
|
'px-0 bg-transparent data-[hover=true]:bg-transparent data-[active=true]:bg-transparent',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
action: 'negative',
|
||||||
|
variant: 'link',
|
||||||
|
class:
|
||||||
|
'px-0 bg-transparent data-[hover=true]:bg-transparent data-[active=true]:bg-transparent',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
action: 'primary',
|
||||||
|
variant: 'outline',
|
||||||
|
class:
|
||||||
|
'bg-transparent data-[hover=true]:bg-background-50 data-[active=true]:bg-transparent',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
action: 'secondary',
|
||||||
|
variant: 'outline',
|
||||||
|
class:
|
||||||
|
'bg-transparent data-[hover=true]:bg-background-50 data-[active=true]:bg-transparent',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
action: 'positive',
|
||||||
|
variant: 'outline',
|
||||||
|
class:
|
||||||
|
'bg-transparent data-[hover=true]:bg-background-50 data-[active=true]:bg-transparent',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
action: 'negative',
|
||||||
|
variant: 'outline',
|
||||||
|
class:
|
||||||
|
'bg-transparent data-[hover=true]:bg-background-50 data-[active=true]:bg-transparent',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
const buttonTextStyle = tva({
|
||||||
|
base: 'text-typography-0 font-semibold web:select-none',
|
||||||
|
parentVariants: {
|
||||||
|
action: {
|
||||||
|
primary:
|
||||||
|
'text-primary-600 data-[hover=true]:text-primary-600 data-[active=true]:text-primary-700',
|
||||||
|
secondary:
|
||||||
|
'text-typography-500 data-[hover=true]:text-typography-600 data-[active=true]:text-typography-700',
|
||||||
|
positive:
|
||||||
|
'text-success-600 data-[hover=true]:text-success-600 data-[active=true]:text-success-700',
|
||||||
|
negative:
|
||||||
|
'text-error-600 data-[hover=true]:text-error-600 data-[active=true]:text-error-700',
|
||||||
|
},
|
||||||
|
variant: {
|
||||||
|
link: 'data-[hover=true]:underline data-[active=true]:underline',
|
||||||
|
outline: '',
|
||||||
|
solid:
|
||||||
|
'text-typography-0 data-[hover=true]:text-typography-0 data-[active=true]:text-typography-0',
|
||||||
|
},
|
||||||
|
size: {
|
||||||
|
xs: 'text-xs',
|
||||||
|
sm: 'text-sm',
|
||||||
|
md: 'text-base',
|
||||||
|
lg: 'text-lg',
|
||||||
|
xl: 'text-xl',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
parentCompoundVariants: [
|
||||||
|
{
|
||||||
|
variant: 'solid',
|
||||||
|
action: 'primary',
|
||||||
|
class:
|
||||||
|
'text-typography-0 data-[hover=true]:text-typography-0 data-[active=true]:text-typography-0',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
variant: 'solid',
|
||||||
|
action: 'secondary',
|
||||||
|
class:
|
||||||
|
'text-typography-800 data-[hover=true]:text-typography-800 data-[active=true]:text-typography-800',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
variant: 'solid',
|
||||||
|
action: 'positive',
|
||||||
|
class:
|
||||||
|
'text-typography-0 data-[hover=true]:text-typography-0 data-[active=true]:text-typography-0',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
variant: 'solid',
|
||||||
|
action: 'negative',
|
||||||
|
class:
|
||||||
|
'text-typography-0 data-[hover=true]:text-typography-0 data-[active=true]:text-typography-0',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
variant: 'outline',
|
||||||
|
action: 'primary',
|
||||||
|
class:
|
||||||
|
'text-primary-500 data-[hover=true]:text-primary-500 data-[active=true]:text-primary-500',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
variant: 'outline',
|
||||||
|
action: 'secondary',
|
||||||
|
class:
|
||||||
|
'text-typography-500 data-[hover=true]:text-primary-600 data-[active=true]:text-typography-700',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
variant: 'outline',
|
||||||
|
action: 'positive',
|
||||||
|
class:
|
||||||
|
'text-primary-500 data-[hover=true]:text-primary-500 data-[active=true]:text-primary-500',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
variant: 'outline',
|
||||||
|
action: 'negative',
|
||||||
|
class:
|
||||||
|
'text-primary-500 data-[hover=true]:text-primary-500 data-[active=true]:text-primary-500',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
const buttonIconStyle = tva({
|
||||||
|
base: 'fill-none',
|
||||||
|
parentVariants: {
|
||||||
|
variant: {
|
||||||
|
link: 'data-[hover=true]:underline data-[active=true]:underline',
|
||||||
|
outline: '',
|
||||||
|
solid:
|
||||||
|
'text-typography-0 data-[hover=true]:text-typography-0 data-[active=true]:text-typography-0',
|
||||||
|
},
|
||||||
|
size: {
|
||||||
|
xs: 'h-3.5 w-3.5',
|
||||||
|
sm: 'h-4 w-4',
|
||||||
|
md: 'h-[18px] w-[18px]',
|
||||||
|
lg: 'h-[18px] w-[18px]',
|
||||||
|
xl: 'h-5 w-5',
|
||||||
|
},
|
||||||
|
action: {
|
||||||
|
primary:
|
||||||
|
'text-primary-600 data-[hover=true]:text-primary-600 data-[active=true]:text-primary-700',
|
||||||
|
secondary:
|
||||||
|
'text-typography-500 data-[hover=true]:text-typography-600 data-[active=true]:text-typography-700',
|
||||||
|
positive:
|
||||||
|
'text-success-600 data-[hover=true]:text-success-600 data-[active=true]:text-success-700',
|
||||||
|
|
||||||
|
negative:
|
||||||
|
'text-error-600 data-[hover=true]:text-error-600 data-[active=true]:text-error-700',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
parentCompoundVariants: [
|
||||||
|
{
|
||||||
|
variant: 'solid',
|
||||||
|
action: 'primary',
|
||||||
|
class:
|
||||||
|
'text-typography-0 data-[hover=true]:text-typography-0 data-[active=true]:text-typography-0',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
variant: 'solid',
|
||||||
|
action: 'secondary',
|
||||||
|
class:
|
||||||
|
'text-typography-800 data-[hover=true]:text-typography-800 data-[active=true]:text-typography-800',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
variant: 'solid',
|
||||||
|
action: 'positive',
|
||||||
|
class:
|
||||||
|
'text-typography-0 data-[hover=true]:text-typography-0 data-[active=true]:text-typography-0',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
variant: 'solid',
|
||||||
|
action: 'negative',
|
||||||
|
class:
|
||||||
|
'text-typography-0 data-[hover=true]:text-typography-0 data-[active=true]:text-typography-0',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
const buttonGroupStyle = tva({
|
||||||
|
base: '',
|
||||||
|
variants: {
|
||||||
|
space: {
|
||||||
|
'xs': 'gap-1',
|
||||||
|
'sm': 'gap-2',
|
||||||
|
'md': 'gap-3',
|
||||||
|
'lg': 'gap-4',
|
||||||
|
'xl': 'gap-5',
|
||||||
|
'2xl': 'gap-6',
|
||||||
|
'3xl': 'gap-7',
|
||||||
|
'4xl': 'gap-8',
|
||||||
|
},
|
||||||
|
isAttached: {
|
||||||
|
true: 'gap-0',
|
||||||
|
},
|
||||||
|
flexDirection: {
|
||||||
|
'row': 'flex-row',
|
||||||
|
'column': 'flex-col',
|
||||||
|
'row-reverse': 'flex-row-reverse',
|
||||||
|
'column-reverse': 'flex-col-reverse',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
type IButtonProps = Omit<
|
||||||
|
React.ComponentPropsWithoutRef<typeof UIButton>,
|
||||||
|
'context'
|
||||||
|
> &
|
||||||
|
VariantProps<typeof buttonStyle> & { className?: string };
|
||||||
|
|
||||||
|
const Button = React.forwardRef<
|
||||||
|
React.ElementRef<typeof UIButton>,
|
||||||
|
IButtonProps
|
||||||
|
>(
|
||||||
|
(
|
||||||
|
{ className, variant = 'solid', size = 'md', action = 'primary', ...props },
|
||||||
|
ref
|
||||||
|
) => {
|
||||||
|
return (
|
||||||
|
<UIButton
|
||||||
|
ref={ref}
|
||||||
|
{...props}
|
||||||
|
className={buttonStyle({ variant, size, action, class: className })}
|
||||||
|
context={{ variant, size, action }}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
type IButtonTextProps = React.ComponentPropsWithoutRef<typeof UIButton.Text> &
|
||||||
|
VariantProps<typeof buttonTextStyle> & { className?: string };
|
||||||
|
|
||||||
|
const ButtonText = React.forwardRef<
|
||||||
|
React.ElementRef<typeof UIButton.Text>,
|
||||||
|
IButtonTextProps
|
||||||
|
>(({ className, variant, size, action, ...props }, ref) => {
|
||||||
|
const {
|
||||||
|
variant: parentVariant,
|
||||||
|
size: parentSize,
|
||||||
|
action: parentAction,
|
||||||
|
} = useStyleContext(SCOPE);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<UIButton.Text
|
||||||
|
ref={ref}
|
||||||
|
{...props}
|
||||||
|
className={buttonTextStyle({
|
||||||
|
parentVariants: {
|
||||||
|
variant: parentVariant,
|
||||||
|
size: parentSize,
|
||||||
|
action: parentAction,
|
||||||
|
},
|
||||||
|
variant,
|
||||||
|
size,
|
||||||
|
action,
|
||||||
|
class: className,
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
const ButtonSpinner = UIButton.Spinner;
|
||||||
|
|
||||||
|
type IButtonIcon = React.ComponentPropsWithoutRef<typeof UIButton.Icon> &
|
||||||
|
VariantProps<typeof buttonIconStyle> & {
|
||||||
|
className?: string | undefined;
|
||||||
|
as?: React.ElementType;
|
||||||
|
height?: number;
|
||||||
|
width?: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
const ButtonIcon = React.forwardRef<
|
||||||
|
React.ElementRef<typeof UIButton.Icon>,
|
||||||
|
IButtonIcon
|
||||||
|
>(({ className, size, ...props }, ref) => {
|
||||||
|
const {
|
||||||
|
variant: parentVariant,
|
||||||
|
size: parentSize,
|
||||||
|
action: parentAction,
|
||||||
|
} = useStyleContext(SCOPE);
|
||||||
|
|
||||||
|
if (typeof size === 'number') {
|
||||||
|
return (
|
||||||
|
<UIButton.Icon
|
||||||
|
ref={ref}
|
||||||
|
{...props}
|
||||||
|
className={buttonIconStyle({ class: className })}
|
||||||
|
size={size}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
} else if (
|
||||||
|
(props.height !== undefined || props.width !== undefined) &&
|
||||||
|
size === undefined
|
||||||
|
) {
|
||||||
|
return (
|
||||||
|
<UIButton.Icon
|
||||||
|
ref={ref}
|
||||||
|
{...props}
|
||||||
|
className={buttonIconStyle({ class: className })}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<UIButton.Icon
|
||||||
|
{...props}
|
||||||
|
className={buttonIconStyle({
|
||||||
|
parentVariants: {
|
||||||
|
size: parentSize,
|
||||||
|
variant: parentVariant,
|
||||||
|
action: parentAction,
|
||||||
|
},
|
||||||
|
size,
|
||||||
|
class: className,
|
||||||
|
})}
|
||||||
|
ref={ref}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
type IButtonGroupProps = React.ComponentPropsWithoutRef<typeof UIButton.Group> &
|
||||||
|
VariantProps<typeof buttonGroupStyle>;
|
||||||
|
|
||||||
|
const ButtonGroup = React.forwardRef<
|
||||||
|
React.ElementRef<typeof UIButton.Group>,
|
||||||
|
IButtonGroupProps
|
||||||
|
>(
|
||||||
|
(
|
||||||
|
{
|
||||||
|
className,
|
||||||
|
space = 'md',
|
||||||
|
isAttached = false,
|
||||||
|
flexDirection = 'column',
|
||||||
|
...props
|
||||||
|
},
|
||||||
|
ref
|
||||||
|
) => {
|
||||||
|
return (
|
||||||
|
<UIButton.Group
|
||||||
|
className={buttonGroupStyle({
|
||||||
|
class: className,
|
||||||
|
space,
|
||||||
|
isAttached,
|
||||||
|
flexDirection,
|
||||||
|
})}
|
||||||
|
{...props}
|
||||||
|
ref={ref}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
Button.displayName = 'Button';
|
||||||
|
ButtonText.displayName = 'ButtonText';
|
||||||
|
ButtonSpinner.displayName = 'ButtonSpinner';
|
||||||
|
ButtonIcon.displayName = 'ButtonIcon';
|
||||||
|
ButtonGroup.displayName = 'ButtonGroup';
|
||||||
|
|
||||||
|
export { Button, ButtonText, ButtonSpinner, ButtonIcon, ButtonGroup };
|
||||||
19
components/ui/content.tsx
Normal file
19
components/ui/content.tsx
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
import { ContentPadding } from "@/constants/theme";
|
||||||
|
import { ReactNode } from "react";
|
||||||
|
import { ScrollView, StyleSheet } from "react-native";
|
||||||
|
|
||||||
|
interface ContentProps {
|
||||||
|
children?: ReactNode
|
||||||
|
}
|
||||||
|
|
||||||
|
const Content = ({ children }: ContentProps) => {
|
||||||
|
return <ScrollView style={styles.content}>{ children }</ScrollView>
|
||||||
|
}
|
||||||
|
|
||||||
|
const styles = StyleSheet.create({
|
||||||
|
content: {
|
||||||
|
padding: ContentPadding
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
export default Content;
|
||||||
40
components/ui/divider/index.tsx
Normal file
40
components/ui/divider/index.tsx
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
'use client';
|
||||||
|
import React from 'react';
|
||||||
|
import { tva } from '@gluestack-ui/utils/nativewind-utils';
|
||||||
|
import { Platform, View } from 'react-native';
|
||||||
|
import type { VariantProps } from '@gluestack-ui/utils/nativewind-utils';
|
||||||
|
|
||||||
|
const dividerStyle = tva({
|
||||||
|
base: 'bg-background-200',
|
||||||
|
variants: {
|
||||||
|
orientation: {
|
||||||
|
vertical: 'w-px h-full',
|
||||||
|
horizontal: 'h-px w-full',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
type IUIDividerProps = React.ComponentPropsWithoutRef<typeof View> &
|
||||||
|
VariantProps<typeof dividerStyle>;
|
||||||
|
|
||||||
|
const Divider = React.forwardRef<
|
||||||
|
React.ComponentRef<typeof View>,
|
||||||
|
IUIDividerProps
|
||||||
|
>(function Divider({ className, orientation = 'horizontal', ...props }, ref) {
|
||||||
|
return (
|
||||||
|
<View
|
||||||
|
ref={ref}
|
||||||
|
{...props}
|
||||||
|
aria-orientation={orientation}
|
||||||
|
role={Platform.OS === 'web' ? 'separator' : undefined}
|
||||||
|
className={dividerStyle({
|
||||||
|
orientation,
|
||||||
|
class: className,
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
Divider.displayName = 'Divider';
|
||||||
|
|
||||||
|
export { Divider };
|
||||||
17
components/ui/error-text.tsx
Normal file
17
components/ui/error-text.tsx
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
import { useThemeColor } from "@/hooks/use-theme-color";
|
||||||
|
import { ReactNode } from "react";
|
||||||
|
import { ThemedText } from "./themed-text";
|
||||||
|
|
||||||
|
interface ErrorTextProps {
|
||||||
|
children?: ReactNode
|
||||||
|
}
|
||||||
|
|
||||||
|
const ErrorText = ({ children }: ErrorTextProps) => {
|
||||||
|
const color = useThemeColor({ }, 'errorText');
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ThemedText lightColor={color} darkColor={color}>{children}</ThemedText>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ErrorText
|
||||||
468
components/ui/form-control/index.tsx
Normal file
468
components/ui/form-control/index.tsx
Normal file
@ -0,0 +1,468 @@
|
|||||||
|
'use client';
|
||||||
|
import { Text, View } from 'react-native';
|
||||||
|
import React from 'react';
|
||||||
|
import { createFormControl } from '@gluestack-ui/core/form-control/creator';
|
||||||
|
import { tva } from '@gluestack-ui/utils/nativewind-utils';
|
||||||
|
import {
|
||||||
|
withStyleContext,
|
||||||
|
useStyleContext,
|
||||||
|
} from '@gluestack-ui/utils/nativewind-utils';
|
||||||
|
import { cssInterop } from 'nativewind';
|
||||||
|
import type { VariantProps } from '@gluestack-ui/utils/nativewind-utils';
|
||||||
|
import { PrimitiveIcon, UIIcon } from '@gluestack-ui/core/icon/creator';
|
||||||
|
|
||||||
|
const SCOPE = 'FORM_CONTROL';
|
||||||
|
|
||||||
|
const formControlStyle = tva({
|
||||||
|
base: 'flex flex-col',
|
||||||
|
variants: {
|
||||||
|
size: {
|
||||||
|
sm: '',
|
||||||
|
md: '',
|
||||||
|
lg: '',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const formControlErrorIconStyle = tva({
|
||||||
|
base: 'text-error-700 fill-none',
|
||||||
|
variants: {
|
||||||
|
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 formControlErrorStyle = tva({
|
||||||
|
base: 'flex flex-row justify-start items-center mt-1 gap-1',
|
||||||
|
});
|
||||||
|
|
||||||
|
const formControlErrorTextStyle = tva({
|
||||||
|
base: 'text-error-700',
|
||||||
|
variants: {
|
||||||
|
isTruncated: {
|
||||||
|
true: 'web:truncate',
|
||||||
|
},
|
||||||
|
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',
|
||||||
|
},
|
||||||
|
sub: {
|
||||||
|
true: 'text-xs',
|
||||||
|
},
|
||||||
|
italic: {
|
||||||
|
true: 'italic',
|
||||||
|
},
|
||||||
|
highlight: {
|
||||||
|
true: 'bg-yellow-500',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const formControlHelperStyle = tva({
|
||||||
|
base: 'flex flex-row justify-start items-center mt-1',
|
||||||
|
});
|
||||||
|
|
||||||
|
const formControlHelperTextStyle = tva({
|
||||||
|
base: 'text-typography-500',
|
||||||
|
variants: {
|
||||||
|
isTruncated: {
|
||||||
|
true: 'web:truncate',
|
||||||
|
},
|
||||||
|
bold: {
|
||||||
|
true: 'font-bold',
|
||||||
|
},
|
||||||
|
underline: {
|
||||||
|
true: 'underline',
|
||||||
|
},
|
||||||
|
strikeThrough: {
|
||||||
|
true: 'line-through',
|
||||||
|
},
|
||||||
|
size: {
|
||||||
|
'2xs': 'text-2xs',
|
||||||
|
'xs': 'text-xs',
|
||||||
|
'sm': 'text-xs',
|
||||||
|
'md': 'text-sm',
|
||||||
|
'lg': 'text-base',
|
||||||
|
'xl': 'text-xl',
|
||||||
|
'2xl': 'text-2xl',
|
||||||
|
'3xl': 'text-3xl',
|
||||||
|
'4xl': 'text-4xl',
|
||||||
|
'5xl': 'text-5xl',
|
||||||
|
'6xl': 'text-6xl',
|
||||||
|
},
|
||||||
|
sub: {
|
||||||
|
true: 'text-xs',
|
||||||
|
},
|
||||||
|
italic: {
|
||||||
|
true: 'italic',
|
||||||
|
},
|
||||||
|
highlight: {
|
||||||
|
true: 'bg-yellow-500',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const formControlLabelStyle = tva({
|
||||||
|
base: 'flex flex-row justify-start items-center mb-1',
|
||||||
|
});
|
||||||
|
|
||||||
|
const formControlLabelTextStyle = tva({
|
||||||
|
base: 'font-medium text-typography-900',
|
||||||
|
variants: {
|
||||||
|
isTruncated: {
|
||||||
|
true: 'web:truncate',
|
||||||
|
},
|
||||||
|
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',
|
||||||
|
},
|
||||||
|
sub: {
|
||||||
|
true: 'text-xs',
|
||||||
|
},
|
||||||
|
italic: {
|
||||||
|
true: 'italic',
|
||||||
|
},
|
||||||
|
highlight: {
|
||||||
|
true: 'bg-yellow-500',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const formControlLabelAstrickStyle = tva({
|
||||||
|
base: 'font-medium text-typography-900',
|
||||||
|
variants: {
|
||||||
|
isTruncated: {
|
||||||
|
true: 'web:truncate',
|
||||||
|
},
|
||||||
|
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',
|
||||||
|
},
|
||||||
|
sub: {
|
||||||
|
true: 'text-xs',
|
||||||
|
},
|
||||||
|
italic: {
|
||||||
|
true: 'italic',
|
||||||
|
},
|
||||||
|
highlight: {
|
||||||
|
true: 'bg-yellow-500',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
type IFormControlLabelAstrickProps = React.ComponentPropsWithoutRef<
|
||||||
|
typeof Text
|
||||||
|
> &
|
||||||
|
VariantProps<typeof formControlLabelAstrickStyle>;
|
||||||
|
|
||||||
|
const FormControlLabelAstrick = React.forwardRef<
|
||||||
|
React.ComponentRef<typeof Text>,
|
||||||
|
IFormControlLabelAstrickProps
|
||||||
|
>(function FormControlLabelAstrick({ className, ...props }, ref) {
|
||||||
|
const { size: parentSize } = useStyleContext(SCOPE);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Text
|
||||||
|
ref={ref}
|
||||||
|
className={formControlLabelAstrickStyle({
|
||||||
|
parentVariants: { size: parentSize },
|
||||||
|
class: className,
|
||||||
|
})}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
export const UIFormControl = createFormControl({
|
||||||
|
Root: withStyleContext(View, SCOPE),
|
||||||
|
Error: View,
|
||||||
|
ErrorText: Text,
|
||||||
|
ErrorIcon: UIIcon,
|
||||||
|
Label: View,
|
||||||
|
LabelText: Text,
|
||||||
|
LabelAstrick: FormControlLabelAstrick,
|
||||||
|
Helper: View,
|
||||||
|
HelperText: Text,
|
||||||
|
});
|
||||||
|
|
||||||
|
cssInterop(PrimitiveIcon, {
|
||||||
|
className: {
|
||||||
|
target: 'style',
|
||||||
|
nativeStyleToProp: {
|
||||||
|
height: true,
|
||||||
|
width: true,
|
||||||
|
fill: true,
|
||||||
|
color: true,
|
||||||
|
stroke: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
type IFormControlProps = React.ComponentProps<typeof UIFormControl> &
|
||||||
|
VariantProps<typeof formControlStyle>;
|
||||||
|
|
||||||
|
const FormControl = React.forwardRef<
|
||||||
|
React.ComponentRef<typeof UIFormControl>,
|
||||||
|
IFormControlProps
|
||||||
|
>(function FormControl({ className, size = 'md', ...props }, ref) {
|
||||||
|
return (
|
||||||
|
<UIFormControl
|
||||||
|
ref={ref}
|
||||||
|
className={formControlStyle({ size, class: className })}
|
||||||
|
{...props}
|
||||||
|
context={{ size }}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
type IFormControlErrorProps = React.ComponentProps<typeof UIFormControl.Error> &
|
||||||
|
VariantProps<typeof formControlErrorStyle>;
|
||||||
|
|
||||||
|
const FormControlError = React.forwardRef<
|
||||||
|
React.ComponentRef<typeof UIFormControl.Error>,
|
||||||
|
IFormControlErrorProps
|
||||||
|
>(function FormControlError({ className, ...props }, ref) {
|
||||||
|
return (
|
||||||
|
<UIFormControl.Error
|
||||||
|
ref={ref}
|
||||||
|
className={formControlErrorStyle({ class: className })}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
type IFormControlErrorTextProps = React.ComponentProps<
|
||||||
|
typeof UIFormControl.Error.Text
|
||||||
|
> &
|
||||||
|
VariantProps<typeof formControlErrorTextStyle>;
|
||||||
|
|
||||||
|
const FormControlErrorText = React.forwardRef<
|
||||||
|
React.ComponentRef<typeof UIFormControl.Error.Text>,
|
||||||
|
IFormControlErrorTextProps
|
||||||
|
>(function FormControlErrorText({ className, size, ...props }, ref) {
|
||||||
|
const { size: parentSize } = useStyleContext(SCOPE);
|
||||||
|
return (
|
||||||
|
<UIFormControl.Error.Text
|
||||||
|
className={formControlErrorTextStyle({
|
||||||
|
parentVariants: { size: parentSize },
|
||||||
|
size,
|
||||||
|
class: className,
|
||||||
|
})}
|
||||||
|
ref={ref}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
type IFormControlErrorIconProps = React.ComponentProps<
|
||||||
|
typeof UIFormControl.Error.Icon
|
||||||
|
> &
|
||||||
|
VariantProps<typeof formControlErrorIconStyle> & {
|
||||||
|
height?: number;
|
||||||
|
width?: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
const FormControlErrorIcon = React.forwardRef<
|
||||||
|
React.ComponentRef<typeof UIFormControl.Error.Icon>,
|
||||||
|
IFormControlErrorIconProps
|
||||||
|
>(function FormControlErrorIcon({ className, size, ...props }, ref) {
|
||||||
|
const { size: parentSize } = useStyleContext(SCOPE);
|
||||||
|
|
||||||
|
if (typeof size === 'number') {
|
||||||
|
return (
|
||||||
|
<UIFormControl.Error.Icon
|
||||||
|
ref={ref}
|
||||||
|
{...props}
|
||||||
|
className={formControlErrorIconStyle({ class: className })}
|
||||||
|
size={size}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
} else if (
|
||||||
|
(props.height !== undefined || props.width !== undefined) &&
|
||||||
|
size === undefined
|
||||||
|
) {
|
||||||
|
return (
|
||||||
|
<UIFormControl.Error.Icon
|
||||||
|
ref={ref}
|
||||||
|
{...props}
|
||||||
|
className={formControlErrorIconStyle({ class: className })}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<UIFormControl.Error.Icon
|
||||||
|
className={formControlErrorIconStyle({
|
||||||
|
parentVariants: { size: parentSize },
|
||||||
|
size,
|
||||||
|
class: className,
|
||||||
|
})}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
type IFormControlLabelProps = React.ComponentProps<typeof UIFormControl.Label> &
|
||||||
|
VariantProps<typeof formControlLabelStyle>;
|
||||||
|
|
||||||
|
const FormControlLabel = React.forwardRef<
|
||||||
|
React.ComponentRef<typeof UIFormControl.Label>,
|
||||||
|
IFormControlLabelProps
|
||||||
|
>(function FormControlLabel({ className, ...props }, ref) {
|
||||||
|
return (
|
||||||
|
<UIFormControl.Label
|
||||||
|
ref={ref}
|
||||||
|
className={formControlLabelStyle({ class: className })}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
type IFormControlLabelTextProps = React.ComponentProps<
|
||||||
|
typeof UIFormControl.Label.Text
|
||||||
|
> &
|
||||||
|
VariantProps<typeof formControlLabelTextStyle>;
|
||||||
|
|
||||||
|
const FormControlLabelText = React.forwardRef<
|
||||||
|
React.ComponentRef<typeof UIFormControl.Label.Text>,
|
||||||
|
IFormControlLabelTextProps
|
||||||
|
>(function FormControlLabelText({ className, size, ...props }, ref) {
|
||||||
|
const { size: parentSize } = useStyleContext(SCOPE);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<UIFormControl.Label.Text
|
||||||
|
className={formControlLabelTextStyle({
|
||||||
|
parentVariants: { size: parentSize },
|
||||||
|
size,
|
||||||
|
class: className,
|
||||||
|
})}
|
||||||
|
ref={ref}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
type IFormControlHelperProps = React.ComponentProps<
|
||||||
|
typeof UIFormControl.Helper
|
||||||
|
> &
|
||||||
|
VariantProps<typeof formControlHelperStyle>;
|
||||||
|
|
||||||
|
const FormControlHelper = React.forwardRef<
|
||||||
|
React.ComponentRef<typeof UIFormControl.Helper>,
|
||||||
|
IFormControlHelperProps
|
||||||
|
>(function FormControlHelper({ className, ...props }, ref) {
|
||||||
|
return (
|
||||||
|
<UIFormControl.Helper
|
||||||
|
ref={ref}
|
||||||
|
className={formControlHelperStyle({
|
||||||
|
class: className,
|
||||||
|
})}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
type IFormControlHelperTextProps = React.ComponentProps<
|
||||||
|
typeof UIFormControl.Helper.Text
|
||||||
|
> &
|
||||||
|
VariantProps<typeof formControlHelperTextStyle>;
|
||||||
|
|
||||||
|
const FormControlHelperText = React.forwardRef<
|
||||||
|
React.ComponentRef<typeof UIFormControl.Helper.Text>,
|
||||||
|
IFormControlHelperTextProps
|
||||||
|
>(function FormControlHelperText({ className, size, ...props }, ref) {
|
||||||
|
const { size: parentSize } = useStyleContext(SCOPE);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<UIFormControl.Helper.Text
|
||||||
|
className={formControlHelperTextStyle({
|
||||||
|
parentVariants: { size: parentSize },
|
||||||
|
size,
|
||||||
|
class: className,
|
||||||
|
})}
|
||||||
|
ref={ref}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
FormControl.displayName = 'FormControl';
|
||||||
|
FormControlError.displayName = 'FormControlError';
|
||||||
|
FormControlErrorText.displayName = 'FormControlErrorText';
|
||||||
|
FormControlErrorIcon.displayName = 'FormControlErrorIcon';
|
||||||
|
FormControlLabel.displayName = 'FormControlLabel';
|
||||||
|
FormControlLabelText.displayName = 'FormControlLabelText';
|
||||||
|
FormControlLabelAstrick.displayName = 'FormControlLabelAstrick';
|
||||||
|
FormControlHelper.displayName = 'FormControlHelper';
|
||||||
|
FormControlHelperText.displayName = 'FormControlHelperText';
|
||||||
|
|
||||||
|
export {
|
||||||
|
FormControl,
|
||||||
|
FormControlError,
|
||||||
|
FormControlErrorText,
|
||||||
|
FormControlErrorIcon,
|
||||||
|
FormControlLabel,
|
||||||
|
FormControlLabelText,
|
||||||
|
FormControlLabelAstrick,
|
||||||
|
FormControlHelper,
|
||||||
|
FormControlHelperText,
|
||||||
|
};
|
||||||
52
components/ui/form-input.tsx
Normal file
52
components/ui/form-input.tsx
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
import { ComponentProps } from "react";
|
||||||
|
import { Controller } from "react-hook-form";
|
||||||
|
import { View } from "react-native";
|
||||||
|
import ErrorText from "./error-text";
|
||||||
|
import { Input, InputField } from "./input";
|
||||||
|
import { ThemedText } from "./themed-text";
|
||||||
|
|
||||||
|
interface FormInputProps {
|
||||||
|
name: string;
|
||||||
|
control: any;
|
||||||
|
label?: string;
|
||||||
|
className?: string;
|
||||||
|
inputProps?: ComponentProps<typeof Input>;
|
||||||
|
fieldProps?: ComponentProps<typeof InputField>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const FormInput = ({
|
||||||
|
name,
|
||||||
|
control,
|
||||||
|
inputProps,
|
||||||
|
fieldProps,
|
||||||
|
label,
|
||||||
|
className,
|
||||||
|
}: FormInputProps) => {
|
||||||
|
return (
|
||||||
|
<View className={className}>
|
||||||
|
<Controller
|
||||||
|
name={name}
|
||||||
|
control={control}
|
||||||
|
render={(renderProps) => (
|
||||||
|
<View>
|
||||||
|
{label && (
|
||||||
|
<ThemedText className="font-semibold mb-1">{label}</ThemedText>
|
||||||
|
)}
|
||||||
|
<Input isInvalid={!!renderProps.fieldState.error} {...inputProps}>
|
||||||
|
<InputField
|
||||||
|
onChangeText={renderProps.field.onChange}
|
||||||
|
value={renderProps.field.value}
|
||||||
|
{...fieldProps}
|
||||||
|
/>
|
||||||
|
</Input>
|
||||||
|
{!!renderProps.fieldState.error && (
|
||||||
|
<ErrorText>{renderProps.fieldState.error.message}</ErrorText>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default FormInput;
|
||||||
309
components/ui/gluestack-ui-provider/config.ts
Normal file
309
components/ui/gluestack-ui-provider/config.ts
Normal file
@ -0,0 +1,309 @@
|
|||||||
|
'use client';
|
||||||
|
import { vars } from 'nativewind';
|
||||||
|
|
||||||
|
export const config = {
|
||||||
|
light: vars({
|
||||||
|
'--color-primary-0': '179 179 179',
|
||||||
|
'--color-primary-50': '153 153 153',
|
||||||
|
'--color-primary-100': '128 128 128',
|
||||||
|
'--color-primary-200': '115 115 115',
|
||||||
|
'--color-primary-300': '102 102 102',
|
||||||
|
'--color-primary-400': '82 82 82',
|
||||||
|
'--color-primary-500': '51 51 51',
|
||||||
|
'--color-primary-600': '41 41 41',
|
||||||
|
'--color-primary-700': '31 31 31',
|
||||||
|
'--color-primary-800': '13 13 13',
|
||||||
|
'--color-primary-900': '10 10 10',
|
||||||
|
'--color-primary-950': '8 8 8',
|
||||||
|
|
||||||
|
/* Secondary */
|
||||||
|
'--color-secondary-0': '253 253 253',
|
||||||
|
'--color-secondary-50': '251 251 251',
|
||||||
|
'--color-secondary-100': '246 246 246',
|
||||||
|
'--color-secondary-200': '242 242 242',
|
||||||
|
'--color-secondary-300': '237 237 237',
|
||||||
|
'--color-secondary-400': '230 230 231',
|
||||||
|
'--color-secondary-500': '217 217 219',
|
||||||
|
'--color-secondary-600': '198 199 199',
|
||||||
|
'--color-secondary-700': '189 189 189',
|
||||||
|
'--color-secondary-800': '177 177 177',
|
||||||
|
'--color-secondary-900': '165 164 164',
|
||||||
|
'--color-secondary-950': '157 157 157',
|
||||||
|
|
||||||
|
/* Tertiary */
|
||||||
|
'--color-tertiary-0': '255 250 245',
|
||||||
|
'--color-tertiary-50': '255 242 229',
|
||||||
|
'--color-tertiary-100': '255 233 213',
|
||||||
|
'--color-tertiary-200': '254 209 170',
|
||||||
|
'--color-tertiary-300': '253 180 116',
|
||||||
|
'--color-tertiary-400': '251 157 75',
|
||||||
|
'--color-tertiary-500': '231 129 40',
|
||||||
|
'--color-tertiary-600': '215 117 31',
|
||||||
|
'--color-tertiary-700': '180 98 26',
|
||||||
|
'--color-tertiary-800': '130 73 23',
|
||||||
|
'--color-tertiary-900': '108 61 19',
|
||||||
|
'--color-tertiary-950': '84 49 18',
|
||||||
|
|
||||||
|
/* Error */
|
||||||
|
'--color-error-0': '254 233 233',
|
||||||
|
'--color-error-50': '254 226 226',
|
||||||
|
'--color-error-100': '254 202 202',
|
||||||
|
'--color-error-200': '252 165 165',
|
||||||
|
'--color-error-300': '248 113 113',
|
||||||
|
'--color-error-400': '239 68 68',
|
||||||
|
'--color-error-500': '230 53 53',
|
||||||
|
'--color-error-600': '220 38 38',
|
||||||
|
'--color-error-700': '185 28 28',
|
||||||
|
'--color-error-800': '153 27 27',
|
||||||
|
'--color-error-900': '127 29 29',
|
||||||
|
'--color-error-950': '83 19 19',
|
||||||
|
|
||||||
|
/* Success */
|
||||||
|
'--color-success-0': '228 255 244',
|
||||||
|
'--color-success-50': '202 255 232',
|
||||||
|
'--color-success-100': '162 241 192',
|
||||||
|
'--color-success-200': '132 211 162',
|
||||||
|
'--color-success-300': '102 181 132',
|
||||||
|
'--color-success-400': '72 151 102',
|
||||||
|
'--color-success-500': '52 131 82',
|
||||||
|
'--color-success-600': '42 121 72',
|
||||||
|
'--color-success-700': '32 111 62',
|
||||||
|
'--color-success-800': '22 101 52',
|
||||||
|
'--color-success-900': '20 83 45',
|
||||||
|
'--color-success-950': '27 50 36',
|
||||||
|
|
||||||
|
/* Warning */
|
||||||
|
'--color-warning-0': '255 249 245',
|
||||||
|
'--color-warning-50': '255 244 236',
|
||||||
|
'--color-warning-100': '255 231 213',
|
||||||
|
'--color-warning-200': '254 205 170',
|
||||||
|
'--color-warning-300': '253 173 116',
|
||||||
|
'--color-warning-400': '251 149 75',
|
||||||
|
'--color-warning-500': '231 120 40',
|
||||||
|
'--color-warning-600': '215 108 31',
|
||||||
|
'--color-warning-700': '180 90 26',
|
||||||
|
'--color-warning-800': '130 68 23',
|
||||||
|
'--color-warning-900': '108 56 19',
|
||||||
|
'--color-warning-950': '84 45 18',
|
||||||
|
|
||||||
|
/* Info */
|
||||||
|
'--color-info-0': '236 248 254',
|
||||||
|
'--color-info-50': '199 235 252',
|
||||||
|
'--color-info-100': '162 221 250',
|
||||||
|
'--color-info-200': '124 207 248',
|
||||||
|
'--color-info-300': '87 194 246',
|
||||||
|
'--color-info-400': '50 180 244',
|
||||||
|
'--color-info-500': '13 166 242',
|
||||||
|
'--color-info-600': '11 141 205',
|
||||||
|
'--color-info-700': '9 115 168',
|
||||||
|
'--color-info-800': '7 90 131',
|
||||||
|
'--color-info-900': '5 64 93',
|
||||||
|
'--color-info-950': '3 38 56',
|
||||||
|
|
||||||
|
/* Typography */
|
||||||
|
'--color-typography-0': '254 254 255',
|
||||||
|
'--color-typography-50': '245 245 245',
|
||||||
|
'--color-typography-100': '229 229 229',
|
||||||
|
'--color-typography-200': '219 219 220',
|
||||||
|
'--color-typography-300': '212 212 212',
|
||||||
|
'--color-typography-400': '163 163 163',
|
||||||
|
'--color-typography-500': '140 140 140',
|
||||||
|
'--color-typography-600': '115 115 115',
|
||||||
|
'--color-typography-700': '82 82 82',
|
||||||
|
'--color-typography-800': '64 64 64',
|
||||||
|
'--color-typography-900': '38 38 39',
|
||||||
|
'--color-typography-950': '23 23 23',
|
||||||
|
|
||||||
|
/* Outline */
|
||||||
|
'--color-outline-0': '253 254 254',
|
||||||
|
'--color-outline-50': '243 243 243',
|
||||||
|
'--color-outline-100': '230 230 230',
|
||||||
|
'--color-outline-200': '221 220 219',
|
||||||
|
'--color-outline-300': '211 211 211',
|
||||||
|
'--color-outline-400': '165 163 163',
|
||||||
|
'--color-outline-500': '140 141 141',
|
||||||
|
'--color-outline-600': '115 116 116',
|
||||||
|
'--color-outline-700': '83 82 82',
|
||||||
|
'--color-outline-800': '65 65 65',
|
||||||
|
'--color-outline-900': '39 38 36',
|
||||||
|
'--color-outline-950': '26 23 23',
|
||||||
|
|
||||||
|
/* Background */
|
||||||
|
'--color-background-0': '255 255 255',
|
||||||
|
'--color-background-50': '246 246 246',
|
||||||
|
'--color-background-100': '242 241 241',
|
||||||
|
'--color-background-200': '220 219 219',
|
||||||
|
'--color-background-300': '213 212 212',
|
||||||
|
'--color-background-400': '162 163 163',
|
||||||
|
'--color-background-500': '142 142 142',
|
||||||
|
'--color-background-600': '116 116 116',
|
||||||
|
'--color-background-700': '83 82 82',
|
||||||
|
'--color-background-800': '65 64 64',
|
||||||
|
'--color-background-900': '39 38 37',
|
||||||
|
'--color-background-950': '18 18 18',
|
||||||
|
|
||||||
|
/* Background Special */
|
||||||
|
'--color-background-error': '254 241 241',
|
||||||
|
'--color-background-warning': '255 243 234',
|
||||||
|
'--color-background-success': '237 252 242',
|
||||||
|
'--color-background-muted': '247 248 247',
|
||||||
|
'--color-background-info': '235 248 254',
|
||||||
|
|
||||||
|
/* Focus Ring Indicator */
|
||||||
|
'--color-indicator-primary': '55 55 55',
|
||||||
|
'--color-indicator-info': '83 153 236',
|
||||||
|
'--color-indicator-error': '185 28 28',
|
||||||
|
}),
|
||||||
|
dark: vars({
|
||||||
|
'--color-primary-0': '166 166 166',
|
||||||
|
'--color-primary-50': '175 175 175',
|
||||||
|
'--color-primary-100': '186 186 186',
|
||||||
|
'--color-primary-200': '197 197 197',
|
||||||
|
'--color-primary-300': '212 212 212',
|
||||||
|
'--color-primary-400': '221 221 221',
|
||||||
|
'--color-primary-500': '230 230 230',
|
||||||
|
'--color-primary-600': '240 240 240',
|
||||||
|
'--color-primary-700': '250 250 250',
|
||||||
|
'--color-primary-800': '253 253 253',
|
||||||
|
'--color-primary-900': '254 249 249',
|
||||||
|
'--color-primary-950': '253 252 252',
|
||||||
|
|
||||||
|
/* Secondary */
|
||||||
|
'--color-secondary-0': '20 20 20',
|
||||||
|
'--color-secondary-50': '23 23 23',
|
||||||
|
'--color-secondary-100': '31 31 31',
|
||||||
|
'--color-secondary-200': '39 39 39',
|
||||||
|
'--color-secondary-300': '44 44 44',
|
||||||
|
'--color-secondary-400': '56 57 57',
|
||||||
|
'--color-secondary-500': '63 64 64',
|
||||||
|
'--color-secondary-600': '86 86 86',
|
||||||
|
'--color-secondary-700': '110 110 110',
|
||||||
|
'--color-secondary-800': '135 135 135',
|
||||||
|
'--color-secondary-900': '150 150 150',
|
||||||
|
'--color-secondary-950': '164 164 164',
|
||||||
|
|
||||||
|
/* Tertiary */
|
||||||
|
'--color-tertiary-0': '84 49 18',
|
||||||
|
'--color-tertiary-50': '108 61 19',
|
||||||
|
'--color-tertiary-100': '130 73 23',
|
||||||
|
'--color-tertiary-200': '180 98 26',
|
||||||
|
'--color-tertiary-300': '215 117 31',
|
||||||
|
'--color-tertiary-400': '231 129 40',
|
||||||
|
'--color-tertiary-500': '251 157 75',
|
||||||
|
'--color-tertiary-600': '253 180 116',
|
||||||
|
'--color-tertiary-700': '254 209 170',
|
||||||
|
'--color-tertiary-800': '255 233 213',
|
||||||
|
'--color-tertiary-900': '255 242 229',
|
||||||
|
'--color-tertiary-950': '255 250 245',
|
||||||
|
|
||||||
|
/* Error */
|
||||||
|
'--color-error-0': '83 19 19',
|
||||||
|
'--color-error-50': '127 29 29',
|
||||||
|
'--color-error-100': '153 27 27',
|
||||||
|
'--color-error-200': '185 28 28',
|
||||||
|
'--color-error-300': '220 38 38',
|
||||||
|
'--color-error-400': '230 53 53',
|
||||||
|
'--color-error-500': '239 68 68',
|
||||||
|
'--color-error-600': '249 97 96',
|
||||||
|
'--color-error-700': '229 91 90',
|
||||||
|
'--color-error-800': '254 202 202',
|
||||||
|
'--color-error-900': '254 226 226',
|
||||||
|
'--color-error-950': '254 233 233',
|
||||||
|
|
||||||
|
/* Success */
|
||||||
|
'--color-success-0': '27 50 36',
|
||||||
|
'--color-success-50': '20 83 45',
|
||||||
|
'--color-success-100': '22 101 52',
|
||||||
|
'--color-success-200': '32 111 62',
|
||||||
|
'--color-success-300': '42 121 72',
|
||||||
|
'--color-success-400': '52 131 82',
|
||||||
|
'--color-success-500': '72 151 102',
|
||||||
|
'--color-success-600': '102 181 132',
|
||||||
|
'--color-success-700': '132 211 162',
|
||||||
|
'--color-success-800': '162 241 192',
|
||||||
|
'--color-success-900': '202 255 232',
|
||||||
|
'--color-success-950': '228 255 244',
|
||||||
|
|
||||||
|
/* Warning */
|
||||||
|
'--color-warning-0': '84 45 18',
|
||||||
|
'--color-warning-50': '108 56 19',
|
||||||
|
'--color-warning-100': '130 68 23',
|
||||||
|
'--color-warning-200': '180 90 26',
|
||||||
|
'--color-warning-300': '215 108 31',
|
||||||
|
'--color-warning-400': '231 120 40',
|
||||||
|
'--color-warning-500': '251 149 75',
|
||||||
|
'--color-warning-600': '253 173 116',
|
||||||
|
'--color-warning-700': '254 205 170',
|
||||||
|
'--color-warning-800': '255 231 213',
|
||||||
|
'--color-warning-900': '255 244 237',
|
||||||
|
'--color-warning-950': '255 249 245',
|
||||||
|
|
||||||
|
/* Info */
|
||||||
|
'--color-info-0': '3 38 56',
|
||||||
|
'--color-info-50': '5 64 93',
|
||||||
|
'--color-info-100': '7 90 131',
|
||||||
|
'--color-info-200': '9 115 168',
|
||||||
|
'--color-info-300': '11 141 205',
|
||||||
|
'--color-info-400': '13 166 242',
|
||||||
|
'--color-info-500': '50 180 244',
|
||||||
|
'--color-info-600': '87 194 246',
|
||||||
|
'--color-info-700': '124 207 248',
|
||||||
|
'--color-info-800': '162 221 250',
|
||||||
|
'--color-info-900': '199 235 252',
|
||||||
|
'--color-info-950': '236 248 254',
|
||||||
|
|
||||||
|
/* Typography */
|
||||||
|
'--color-typography-0': '23 23 23',
|
||||||
|
'--color-typography-50': '38 38 39',
|
||||||
|
'--color-typography-100': '64 64 64',
|
||||||
|
'--color-typography-200': '82 82 82',
|
||||||
|
'--color-typography-300': '115 115 115',
|
||||||
|
'--color-typography-400': '140 140 140',
|
||||||
|
'--color-typography-500': '163 163 163',
|
||||||
|
'--color-typography-600': '212 212 212',
|
||||||
|
'--color-typography-700': '219 219 220',
|
||||||
|
'--color-typography-800': '229 229 229',
|
||||||
|
'--color-typography-900': '245 245 245',
|
||||||
|
'--color-typography-950': '254 254 255',
|
||||||
|
|
||||||
|
/* Outline */
|
||||||
|
'--color-outline-0': '26 23 23',
|
||||||
|
'--color-outline-50': '39 38 36',
|
||||||
|
'--color-outline-100': '65 65 65',
|
||||||
|
'--color-outline-200': '83 82 82',
|
||||||
|
'--color-outline-300': '115 116 116',
|
||||||
|
'--color-outline-400': '140 141 141',
|
||||||
|
'--color-outline-500': '165 163 163',
|
||||||
|
'--color-outline-600': '211 211 211',
|
||||||
|
'--color-outline-700': '221 220 219',
|
||||||
|
'--color-outline-800': '230 230 230',
|
||||||
|
'--color-outline-900': '243 243 243',
|
||||||
|
'--color-outline-950': '253 254 254',
|
||||||
|
|
||||||
|
/* Background */
|
||||||
|
'--color-background-0': '18 18 18',
|
||||||
|
'--color-background-50': '39 38 37',
|
||||||
|
'--color-background-100': '65 64 64',
|
||||||
|
'--color-background-200': '83 82 82',
|
||||||
|
'--color-background-300': '116 116 116',
|
||||||
|
'--color-background-400': '142 142 142',
|
||||||
|
'--color-background-500': '162 163 163',
|
||||||
|
'--color-background-600': '213 212 212',
|
||||||
|
'--color-background-700': '229 228 228',
|
||||||
|
'--color-background-800': '242 241 241',
|
||||||
|
'--color-background-900': '246 246 246',
|
||||||
|
'--color-background-950': '255 255 255',
|
||||||
|
|
||||||
|
/* Background Special */
|
||||||
|
'--color-background-error': '66 43 43',
|
||||||
|
'--color-background-warning': '65 47 35',
|
||||||
|
'--color-background-success': '28 43 33',
|
||||||
|
'--color-background-muted': '51 51 51',
|
||||||
|
'--color-background-info': '26 40 46',
|
||||||
|
|
||||||
|
/* Focus Ring Indicator */
|
||||||
|
'--color-indicator-primary': '247 247 247',
|
||||||
|
'--color-indicator-info': '161 199 245',
|
||||||
|
'--color-indicator-error': '232 70 69',
|
||||||
|
}),
|
||||||
|
};
|
||||||
87
components/ui/gluestack-ui-provider/index.next15.tsx
Normal file
87
components/ui/gluestack-ui-provider/index.next15.tsx
Normal file
@ -0,0 +1,87 @@
|
|||||||
|
// This is a Next.js 15 compatible version of the GluestackUIProvider
|
||||||
|
'use client';
|
||||||
|
import React, { useEffect, useLayoutEffect } from 'react';
|
||||||
|
import { config } from './config';
|
||||||
|
import { OverlayProvider } from '@gluestack-ui/core/overlay/creator';
|
||||||
|
import { ToastProvider } from '@gluestack-ui/core/toast/creator';
|
||||||
|
import { setFlushStyles } from '@gluestack-ui/utils/nativewind-utils';
|
||||||
|
import { script } from './script';
|
||||||
|
|
||||||
|
const variableStyleTagId = 'nativewind-style';
|
||||||
|
const createStyle = (styleTagId: string) => {
|
||||||
|
const style = document.createElement('style');
|
||||||
|
style.id = styleTagId;
|
||||||
|
style.appendChild(document.createTextNode(''));
|
||||||
|
return style;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useSafeLayoutEffect =
|
||||||
|
typeof window !== 'undefined' ? useLayoutEffect : useEffect;
|
||||||
|
|
||||||
|
export function GluestackUIProvider({
|
||||||
|
mode = 'light',
|
||||||
|
...props
|
||||||
|
}: {
|
||||||
|
mode?: 'light' | 'dark' | 'system';
|
||||||
|
children?: React.ReactNode;
|
||||||
|
}) {
|
||||||
|
let cssVariablesWithMode = ``;
|
||||||
|
Object.keys(config).forEach((configKey) => {
|
||||||
|
cssVariablesWithMode +=
|
||||||
|
configKey === 'dark' ? `\n .dark {\n ` : `\n:root {\n`;
|
||||||
|
const cssVariables = Object.keys(
|
||||||
|
config[configKey as keyof typeof config]
|
||||||
|
).reduce((acc: string, curr: string) => {
|
||||||
|
acc += `${curr}:${config[configKey as keyof typeof config][curr]}; `;
|
||||||
|
return acc;
|
||||||
|
}, '');
|
||||||
|
cssVariablesWithMode += `${cssVariables} \n}`;
|
||||||
|
});
|
||||||
|
|
||||||
|
setFlushStyles(cssVariablesWithMode);
|
||||||
|
|
||||||
|
const handleMediaQuery = React.useCallback((e: MediaQueryListEvent) => {
|
||||||
|
script(e.matches ? 'dark' : 'light');
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useSafeLayoutEffect(() => {
|
||||||
|
if (mode !== 'system') {
|
||||||
|
const documentElement = document.documentElement;
|
||||||
|
if (documentElement) {
|
||||||
|
documentElement.classList.add(mode);
|
||||||
|
documentElement.classList.remove(mode === 'light' ? 'dark' : 'light');
|
||||||
|
documentElement.style.colorScheme = mode;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [mode]);
|
||||||
|
|
||||||
|
useSafeLayoutEffect(() => {
|
||||||
|
if (mode !== 'system') return;
|
||||||
|
const media = window.matchMedia('(prefers-color-scheme: dark)');
|
||||||
|
|
||||||
|
media.addListener(handleMediaQuery);
|
||||||
|
|
||||||
|
return () => media.removeListener(handleMediaQuery);
|
||||||
|
}, [handleMediaQuery]);
|
||||||
|
|
||||||
|
useSafeLayoutEffect(() => {
|
||||||
|
if (typeof window !== 'undefined') {
|
||||||
|
const documentElement = document.documentElement;
|
||||||
|
if (documentElement) {
|
||||||
|
const head = documentElement.querySelector('head');
|
||||||
|
let style = head?.querySelector(`[id='${variableStyleTagId}']`);
|
||||||
|
if (!style) {
|
||||||
|
style = createStyle(variableStyleTagId);
|
||||||
|
style.innerHTML = cssVariablesWithMode;
|
||||||
|
if (head) head.appendChild(style);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<OverlayProvider>
|
||||||
|
<ToastProvider>{props.children}</ToastProvider>
|
||||||
|
</OverlayProvider>
|
||||||
|
);
|
||||||
|
}
|
||||||
38
components/ui/gluestack-ui-provider/index.tsx
Normal file
38
components/ui/gluestack-ui-provider/index.tsx
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
import React, { useEffect } from 'react';
|
||||||
|
import { config } from './config';
|
||||||
|
import { View, ViewProps } from 'react-native';
|
||||||
|
import { OverlayProvider } from '@gluestack-ui/core/overlay/creator';
|
||||||
|
import { ToastProvider } from '@gluestack-ui/core/toast/creator';
|
||||||
|
import { useColorScheme } from 'nativewind';
|
||||||
|
|
||||||
|
export type ModeType = 'light' | 'dark' | 'system';
|
||||||
|
|
||||||
|
export function GluestackUIProvider({
|
||||||
|
mode = 'light',
|
||||||
|
...props
|
||||||
|
}: {
|
||||||
|
mode?: ModeType;
|
||||||
|
children?: React.ReactNode;
|
||||||
|
style?: ViewProps['style'];
|
||||||
|
}) {
|
||||||
|
const { colorScheme, setColorScheme } = useColorScheme();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setColorScheme(mode);
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [mode]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View
|
||||||
|
style={[
|
||||||
|
config[colorScheme!],
|
||||||
|
{ flex: 1, height: '100%', width: '100%' },
|
||||||
|
props.style,
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<OverlayProvider>
|
||||||
|
<ToastProvider>{props.children}</ToastProvider>
|
||||||
|
</OverlayProvider>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
}
|
||||||
96
components/ui/gluestack-ui-provider/index.web.tsx
Normal file
96
components/ui/gluestack-ui-provider/index.web.tsx
Normal file
@ -0,0 +1,96 @@
|
|||||||
|
'use client';
|
||||||
|
import React, { useEffect, useLayoutEffect } from 'react';
|
||||||
|
import { config } from './config';
|
||||||
|
import { OverlayProvider } from '@gluestack-ui/core/overlay/creator';
|
||||||
|
import { ToastProvider } from '@gluestack-ui/core/toast/creator';
|
||||||
|
import { setFlushStyles } from '@gluestack-ui/utils/nativewind-utils';
|
||||||
|
import { script } from './script';
|
||||||
|
|
||||||
|
export type ModeType = 'light' | 'dark' | 'system';
|
||||||
|
|
||||||
|
const variableStyleTagId = 'nativewind-style';
|
||||||
|
const createStyle = (styleTagId: string) => {
|
||||||
|
const style = document.createElement('style');
|
||||||
|
style.id = styleTagId;
|
||||||
|
style.appendChild(document.createTextNode(''));
|
||||||
|
return style;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useSafeLayoutEffect =
|
||||||
|
typeof window !== 'undefined' ? useLayoutEffect : useEffect;
|
||||||
|
|
||||||
|
export function GluestackUIProvider({
|
||||||
|
mode = 'light',
|
||||||
|
...props
|
||||||
|
}: {
|
||||||
|
mode?: ModeType;
|
||||||
|
children?: React.ReactNode;
|
||||||
|
}) {
|
||||||
|
let cssVariablesWithMode = ``;
|
||||||
|
Object.keys(config).forEach((configKey) => {
|
||||||
|
cssVariablesWithMode +=
|
||||||
|
configKey === 'dark' ? `\n .dark {\n ` : `\n:root {\n`;
|
||||||
|
const cssVariables = Object.keys(
|
||||||
|
config[configKey as keyof typeof config]
|
||||||
|
).reduce((acc: string, curr: string) => {
|
||||||
|
acc += `${curr}:${config[configKey as keyof typeof config][curr]}; `;
|
||||||
|
return acc;
|
||||||
|
}, '');
|
||||||
|
cssVariablesWithMode += `${cssVariables} \n}`;
|
||||||
|
});
|
||||||
|
|
||||||
|
setFlushStyles(cssVariablesWithMode);
|
||||||
|
|
||||||
|
const handleMediaQuery = React.useCallback((e: MediaQueryListEvent) => {
|
||||||
|
script(e.matches ? 'dark' : 'light');
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useSafeLayoutEffect(() => {
|
||||||
|
if (mode !== 'system') {
|
||||||
|
const documentElement = document.documentElement;
|
||||||
|
if (documentElement) {
|
||||||
|
documentElement.classList.add(mode);
|
||||||
|
documentElement.classList.remove(mode === 'light' ? 'dark' : 'light');
|
||||||
|
documentElement.style.colorScheme = mode;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [mode]);
|
||||||
|
|
||||||
|
useSafeLayoutEffect(() => {
|
||||||
|
if (mode !== 'system') return;
|
||||||
|
const media = window.matchMedia('(prefers-color-scheme: dark)');
|
||||||
|
|
||||||
|
media.addListener(handleMediaQuery);
|
||||||
|
|
||||||
|
return () => media.removeListener(handleMediaQuery);
|
||||||
|
}, [handleMediaQuery]);
|
||||||
|
|
||||||
|
useSafeLayoutEffect(() => {
|
||||||
|
if (typeof window !== 'undefined') {
|
||||||
|
const documentElement = document.documentElement;
|
||||||
|
if (documentElement) {
|
||||||
|
const head = documentElement.querySelector('head');
|
||||||
|
let style = head?.querySelector(`[id='${variableStyleTagId}']`);
|
||||||
|
if (!style) {
|
||||||
|
style = createStyle(variableStyleTagId);
|
||||||
|
style.innerHTML = cssVariablesWithMode;
|
||||||
|
if (head) head.appendChild(style);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<script
|
||||||
|
suppressHydrationWarning
|
||||||
|
dangerouslySetInnerHTML={{
|
||||||
|
__html: `(${script.toString()})('${mode}')`,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<OverlayProvider>
|
||||||
|
<ToastProvider>{props.children}</ToastProvider>
|
||||||
|
</OverlayProvider>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
19
components/ui/gluestack-ui-provider/script.ts
Normal file
19
components/ui/gluestack-ui-provider/script.ts
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
export const script = (mode: string) => {
|
||||||
|
const documentElement = document.documentElement;
|
||||||
|
|
||||||
|
function getSystemColorMode() {
|
||||||
|
return window.matchMedia('(prefers-color-scheme: dark)').matches
|
||||||
|
? 'dark'
|
||||||
|
: 'light';
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const isSystem = mode === 'system';
|
||||||
|
const theme = isSystem ? getSystemColorMode() : mode;
|
||||||
|
documentElement.classList.remove(theme === 'light' ? 'dark' : 'light');
|
||||||
|
documentElement.classList.add(theme);
|
||||||
|
documentElement.style.colorScheme = theme;
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
}
|
||||||
|
};
|
||||||
217
components/ui/input/index.tsx
Normal file
217
components/ui/input/index.tsx
Normal file
@ -0,0 +1,217 @@
|
|||||||
|
'use client';
|
||||||
|
import React from 'react';
|
||||||
|
import { createInput } from '@gluestack-ui/core/input/creator';
|
||||||
|
import { View, Pressable, TextInput } from 'react-native';
|
||||||
|
import { tva } from '@gluestack-ui/utils/nativewind-utils';
|
||||||
|
import {
|
||||||
|
withStyleContext,
|
||||||
|
useStyleContext,
|
||||||
|
} from '@gluestack-ui/utils/nativewind-utils';
|
||||||
|
import { cssInterop } from 'nativewind';
|
||||||
|
import type { VariantProps } from '@gluestack-ui/utils/nativewind-utils';
|
||||||
|
import { PrimitiveIcon, UIIcon } from '@gluestack-ui/core/icon/creator';
|
||||||
|
|
||||||
|
const SCOPE = 'INPUT';
|
||||||
|
|
||||||
|
const UIInput = createInput({
|
||||||
|
Root: withStyleContext(View, SCOPE),
|
||||||
|
Icon: UIIcon,
|
||||||
|
Slot: Pressable,
|
||||||
|
Input: TextInput,
|
||||||
|
});
|
||||||
|
|
||||||
|
cssInterop(PrimitiveIcon, {
|
||||||
|
className: {
|
||||||
|
target: 'style',
|
||||||
|
nativeStyleToProp: {
|
||||||
|
height: true,
|
||||||
|
width: true,
|
||||||
|
fill: true,
|
||||||
|
color: 'classNameColor',
|
||||||
|
stroke: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const inputStyle = tva({
|
||||||
|
base: 'border-background-300 flex-row overflow-hidden content-center data-[hover=true]:border-outline-400 data-[focus=true]:border-primary-700 data-[focus=true]:hover:border-primary-700 data-[disabled=true]:opacity-40 data-[disabled=true]:hover:border-background-300 items-center',
|
||||||
|
|
||||||
|
variants: {
|
||||||
|
size: {
|
||||||
|
xl: 'h-12',
|
||||||
|
lg: 'h-11',
|
||||||
|
md: 'h-10',
|
||||||
|
sm: 'h-9',
|
||||||
|
},
|
||||||
|
|
||||||
|
variant: {
|
||||||
|
underlined:
|
||||||
|
'rounded-none border-b data-[invalid=true]:border-b-2 data-[invalid=true]:border-error-700 data-[invalid=true]:hover:border-error-700 data-[invalid=true]:data-[focus=true]:border-error-700 data-[invalid=true]:data-[focus=true]:hover:border-error-700 data-[invalid=true]:data-[disabled=true]:hover:border-error-700',
|
||||||
|
|
||||||
|
outline:
|
||||||
|
'rounded border data-[invalid=true]:border-error-700 data-[invalid=true]:hover:border-error-700 data-[invalid=true]:data-[focus=true]:border-error-700 data-[invalid=true]:data-[focus=true]:hover:border-error-700 data-[invalid=true]:data-[disabled=true]:hover:border-error-700 data-[focus=true]:web:ring-1 data-[focus=true]:web:ring-inset data-[focus=true]:web:ring-indicator-primary data-[invalid=true]:web:ring-1 data-[invalid=true]:web:ring-inset data-[invalid=true]:web:ring-indicator-error data-[invalid=true]:data-[focus=true]:hover:web:ring-1 data-[invalid=true]:data-[focus=true]:hover:web:ring-inset data-[invalid=true]:data-[focus=true]:hover:web:ring-indicator-error data-[invalid=true]:data-[disabled=true]:hover:web:ring-1 data-[invalid=true]:data-[disabled=true]:hover:web:ring-inset data-[invalid=true]:data-[disabled=true]:hover:web:ring-indicator-error',
|
||||||
|
|
||||||
|
rounded:
|
||||||
|
'rounded-full border data-[invalid=true]:border-error-700 data-[invalid=true]:hover:border-error-700 data-[invalid=true]:data-[focus=true]:border-error-700 data-[invalid=true]:data-[focus=true]:hover:border-error-700 data-[invalid=true]:data-[disabled=true]:hover:border-error-700 data-[focus=true]:web:ring-1 data-[focus=true]:web:ring-inset data-[focus=true]:web:ring-indicator-primary data-[invalid=true]:web:ring-1 data-[invalid=true]:web:ring-inset data-[invalid=true]:web:ring-indicator-error data-[invalid=true]:data-[focus=true]:hover:web:ring-1 data-[invalid=true]:data-[focus=true]:hover:web:ring-inset data-[invalid=true]:data-[focus=true]:hover:web:ring-indicator-error data-[invalid=true]:data-[disabled=true]:hover:web:ring-1 data-[invalid=true]:data-[disabled=true]:hover:web:ring-inset data-[invalid=true]:data-[disabled=true]:hover:web:ring-indicator-error',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const inputIconStyle = tva({
|
||||||
|
base: 'justify-center items-center text-typography-400 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 inputSlotStyle = tva({
|
||||||
|
base: 'justify-center items-center web:disabled:cursor-not-allowed',
|
||||||
|
});
|
||||||
|
|
||||||
|
const inputFieldStyle = tva({
|
||||||
|
base: 'flex-1 text-typography-900 py-0 px-3 placeholder:text-typography-500 h-full ios:leading-[0px] web:cursor-text web:data-[disabled=true]:cursor-not-allowed',
|
||||||
|
|
||||||
|
parentVariants: {
|
||||||
|
variant: {
|
||||||
|
underlined: 'web:outline-0 web:outline-none px-0',
|
||||||
|
outline: 'web:outline-0 web:outline-none',
|
||||||
|
rounded: 'web:outline-0 web:outline-none px-4',
|
||||||
|
},
|
||||||
|
|
||||||
|
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',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
type IInputProps = React.ComponentProps<typeof UIInput> &
|
||||||
|
VariantProps<typeof inputStyle> & { className?: string };
|
||||||
|
const Input = React.forwardRef<React.ComponentRef<typeof UIInput>, IInputProps>(
|
||||||
|
function Input(
|
||||||
|
{ className, variant = 'outline', size = 'md', ...props },
|
||||||
|
ref
|
||||||
|
) {
|
||||||
|
return (
|
||||||
|
<UIInput
|
||||||
|
ref={ref}
|
||||||
|
{...props}
|
||||||
|
className={inputStyle({ variant, size, class: className })}
|
||||||
|
context={{ variant, size }}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
type IInputIconProps = React.ComponentProps<typeof UIInput.Icon> &
|
||||||
|
VariantProps<typeof inputIconStyle> & {
|
||||||
|
className?: string;
|
||||||
|
height?: number;
|
||||||
|
width?: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
const InputIcon = React.forwardRef<
|
||||||
|
React.ComponentRef<typeof UIInput.Icon>,
|
||||||
|
IInputIconProps
|
||||||
|
>(function InputIcon({ className, size, ...props }, ref) {
|
||||||
|
const { size: parentSize } = useStyleContext(SCOPE);
|
||||||
|
|
||||||
|
if (typeof size === 'number') {
|
||||||
|
return (
|
||||||
|
<UIInput.Icon
|
||||||
|
ref={ref}
|
||||||
|
{...props}
|
||||||
|
className={inputIconStyle({ class: className })}
|
||||||
|
size={size}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
} else if (
|
||||||
|
(props.height !== undefined || props.width !== undefined) &&
|
||||||
|
size === undefined
|
||||||
|
) {
|
||||||
|
return (
|
||||||
|
<UIInput.Icon
|
||||||
|
ref={ref}
|
||||||
|
{...props}
|
||||||
|
className={inputIconStyle({ class: className })}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<UIInput.Icon
|
||||||
|
ref={ref}
|
||||||
|
{...props}
|
||||||
|
className={inputIconStyle({
|
||||||
|
parentVariants: {
|
||||||
|
size: parentSize,
|
||||||
|
},
|
||||||
|
class: className,
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
type IInputSlotProps = React.ComponentProps<typeof UIInput.Slot> &
|
||||||
|
VariantProps<typeof inputSlotStyle> & { className?: string };
|
||||||
|
|
||||||
|
const InputSlot = React.forwardRef<
|
||||||
|
React.ComponentRef<typeof UIInput.Slot>,
|
||||||
|
IInputSlotProps
|
||||||
|
>(function InputSlot({ className, ...props }, ref) {
|
||||||
|
return (
|
||||||
|
<UIInput.Slot
|
||||||
|
ref={ref}
|
||||||
|
{...props}
|
||||||
|
className={inputSlotStyle({
|
||||||
|
class: className,
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
type IInputFieldProps = React.ComponentProps<typeof UIInput.Input> &
|
||||||
|
VariantProps<typeof inputFieldStyle> & { className?: string };
|
||||||
|
|
||||||
|
const InputField = React.forwardRef<
|
||||||
|
React.ComponentRef<typeof UIInput.Input>,
|
||||||
|
IInputFieldProps
|
||||||
|
>(function InputField({ className, ...props }, ref) {
|
||||||
|
const { variant: parentVariant, size: parentSize } = useStyleContext(SCOPE);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<UIInput.Input
|
||||||
|
ref={ref}
|
||||||
|
{...props}
|
||||||
|
className={inputFieldStyle({
|
||||||
|
parentVariants: {
|
||||||
|
variant: parentVariant,
|
||||||
|
size: parentSize,
|
||||||
|
},
|
||||||
|
class: className,
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
Input.displayName = 'Input';
|
||||||
|
InputIcon.displayName = 'InputIcon';
|
||||||
|
InputSlot.displayName = 'InputSlot';
|
||||||
|
InputField.displayName = 'InputField';
|
||||||
|
|
||||||
|
export { Input, InputField, InputIcon, InputSlot };
|
||||||
64
components/ui/login-form/index.tsx
Normal file
64
components/ui/login-form/index.tsx
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
|
import { useForm } from "react-hook-form";
|
||||||
|
import { View } from "react-native";
|
||||||
|
import { z } from 'zod';
|
||||||
|
import { Button, ButtonSpinner, ButtonText } from "../button";
|
||||||
|
import FormInput from "../form-input";
|
||||||
|
|
||||||
|
export type LoginFormData = {
|
||||||
|
email: string;
|
||||||
|
password: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const loginSchema = z.object({
|
||||||
|
email: z
|
||||||
|
.email()
|
||||||
|
.nonempty("This field is required"),
|
||||||
|
password: z
|
||||||
|
.string()
|
||||||
|
.nonempty("This field is required")
|
||||||
|
});
|
||||||
|
|
||||||
|
interface LoginFormProps {
|
||||||
|
isLoading?: boolean;
|
||||||
|
onSubmit?: (data: LoginFormData) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const LoginForm = ({ onSubmit, isLoading=false }: LoginFormProps) => {
|
||||||
|
const { control, handleSubmit } = useForm<LoginFormData>({
|
||||||
|
resolver: zodResolver(loginSchema),
|
||||||
|
defaultValues: {
|
||||||
|
email: "",
|
||||||
|
password: "",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const onSubmitHandler = (data: LoginFormData) => {
|
||||||
|
if (onSubmit) onSubmit(data);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View>
|
||||||
|
<FormInput
|
||||||
|
label="Email"
|
||||||
|
name="email"
|
||||||
|
control={control}
|
||||||
|
className="mb-2"
|
||||||
|
/>
|
||||||
|
<FormInput
|
||||||
|
label="Password"
|
||||||
|
name="password"
|
||||||
|
className="mb-3"
|
||||||
|
control={control}
|
||||||
|
fieldProps={{
|
||||||
|
type: "password"
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Button className={isLoading ? "opacity-70" : ""} disabled={isLoading} onPress={handleSubmit(onSubmitHandler)}>
|
||||||
|
{isLoading && <ButtonSpinner />}<ButtonText>Sign In</ButtonText>
|
||||||
|
</Button>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default LoginForm;
|
||||||
@ -1,3 +1,4 @@
|
|||||||
|
import { useThemeColor } from "@/hooks/use-theme-color";
|
||||||
import { ReactNode } from "react";
|
import { ReactNode } from "react";
|
||||||
import { StyleSheet, View } from "react-native";
|
import { StyleSheet, View } from "react-native";
|
||||||
|
|
||||||
@ -6,8 +7,10 @@ interface PanelProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const Panel = ({children}: PanelProps) => {
|
const Panel = ({children}: PanelProps) => {
|
||||||
|
const backgroundColor = useThemeColor({ }, 'background');
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View style={styles.panel}>
|
<View style={[styles.panel, { backgroundColor }]}>
|
||||||
{children}
|
{children}
|
||||||
</View>
|
</View>
|
||||||
)
|
)
|
||||||
|
|||||||
135
components/ui/skeleton/index.tsx
Normal file
135
components/ui/skeleton/index.tsx
Normal file
@ -0,0 +1,135 @@
|
|||||||
|
import React, { forwardRef } from 'react';
|
||||||
|
import type { VariantProps } from '@gluestack-ui/utils/nativewind-utils';
|
||||||
|
import { Animated, Easing, Platform, View } from 'react-native';
|
||||||
|
import { skeletonStyle, skeletonTextStyle } from './styles';
|
||||||
|
|
||||||
|
type ISkeletonProps = React.ComponentProps<typeof View> &
|
||||||
|
VariantProps<typeof skeletonStyle> & {
|
||||||
|
isLoaded?: boolean;
|
||||||
|
startColor?: string;
|
||||||
|
speed?: number | string;
|
||||||
|
};
|
||||||
|
|
||||||
|
type ISkeletonTextProps = React.ComponentProps<typeof View> &
|
||||||
|
VariantProps<typeof skeletonTextStyle> & {
|
||||||
|
_lines?: number;
|
||||||
|
isLoaded?: boolean;
|
||||||
|
startColor?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const Skeleton = forwardRef<
|
||||||
|
React.ComponentRef<typeof Animated.View>,
|
||||||
|
ISkeletonProps
|
||||||
|
>(function Skeleton(
|
||||||
|
{
|
||||||
|
className,
|
||||||
|
variant,
|
||||||
|
children,
|
||||||
|
startColor = 'bg-background-200',
|
||||||
|
isLoaded = false,
|
||||||
|
speed = 2,
|
||||||
|
...props
|
||||||
|
},
|
||||||
|
ref
|
||||||
|
) {
|
||||||
|
const pulseAnim = new Animated.Value(1);
|
||||||
|
const customTimingFunction = Easing.bezier(0.4, 0, 0.6, 1);
|
||||||
|
const fadeDuration = 0.6;
|
||||||
|
const animationDuration = (fadeDuration * 10000) / Number(speed); // Convert seconds to milliseconds
|
||||||
|
|
||||||
|
const pulse = Animated.sequence([
|
||||||
|
Animated.timing(pulseAnim, {
|
||||||
|
toValue: 1, // Start with opacity 1
|
||||||
|
duration: animationDuration / 2, // Third of the animation duration
|
||||||
|
easing: customTimingFunction,
|
||||||
|
useNativeDriver: Platform.OS !== 'web',
|
||||||
|
}),
|
||||||
|
Animated.timing(pulseAnim, {
|
||||||
|
toValue: 0.75,
|
||||||
|
duration: animationDuration / 2, // Third of the animation duration
|
||||||
|
easing: customTimingFunction,
|
||||||
|
useNativeDriver: Platform.OS !== 'web',
|
||||||
|
}),
|
||||||
|
Animated.timing(pulseAnim, {
|
||||||
|
toValue: 1,
|
||||||
|
duration: animationDuration / 2, // Third of the animation duration
|
||||||
|
easing: customTimingFunction,
|
||||||
|
useNativeDriver: Platform.OS !== 'web',
|
||||||
|
}),
|
||||||
|
]);
|
||||||
|
|
||||||
|
if (!isLoaded) {
|
||||||
|
Animated.loop(pulse).start();
|
||||||
|
return (
|
||||||
|
<Animated.View
|
||||||
|
style={{ opacity: pulseAnim }}
|
||||||
|
className={`${startColor} ${skeletonStyle({
|
||||||
|
variant,
|
||||||
|
class: className,
|
||||||
|
})}`}
|
||||||
|
{...props}
|
||||||
|
ref={ref}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
Animated.loop(pulse).stop();
|
||||||
|
|
||||||
|
return children;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const SkeletonText = forwardRef<
|
||||||
|
React.ComponentRef<typeof View>,
|
||||||
|
ISkeletonTextProps
|
||||||
|
>(function SkeletonText(
|
||||||
|
{
|
||||||
|
className,
|
||||||
|
_lines,
|
||||||
|
isLoaded = false,
|
||||||
|
startColor = 'bg-background-200',
|
||||||
|
gap = 2,
|
||||||
|
children,
|
||||||
|
...props
|
||||||
|
},
|
||||||
|
ref
|
||||||
|
) {
|
||||||
|
if (!isLoaded) {
|
||||||
|
if (_lines) {
|
||||||
|
return (
|
||||||
|
<View
|
||||||
|
className={`${skeletonTextStyle({
|
||||||
|
gap,
|
||||||
|
})}`}
|
||||||
|
ref={ref}
|
||||||
|
>
|
||||||
|
{Array.from({ length: _lines }).map((_, index) => (
|
||||||
|
<Skeleton
|
||||||
|
key={index}
|
||||||
|
className={`${startColor} ${skeletonTextStyle({
|
||||||
|
class: className,
|
||||||
|
})}`}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return (
|
||||||
|
<Skeleton
|
||||||
|
className={`${startColor} ${skeletonTextStyle({
|
||||||
|
class: className,
|
||||||
|
})}`}
|
||||||
|
{...props}
|
||||||
|
ref={ref}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return children;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
Skeleton.displayName = 'Skeleton';
|
||||||
|
SkeletonText.displayName = 'SkeletonText';
|
||||||
|
|
||||||
|
export { Skeleton, SkeletonText };
|
||||||
103
components/ui/skeleton/index.web.tsx
Normal file
103
components/ui/skeleton/index.web.tsx
Normal file
@ -0,0 +1,103 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { skeletonStyle, skeletonTextStyle } from './styles';
|
||||||
|
|
||||||
|
import type { VariantProps } from '@gluestack-ui/utils/nativewind-utils';
|
||||||
|
|
||||||
|
type ISkeletonProps = React.ComponentPropsWithoutRef<'div'> &
|
||||||
|
VariantProps<typeof skeletonStyle> & {
|
||||||
|
startColor?: string;
|
||||||
|
isLoaded?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
const Skeleton = React.forwardRef<HTMLDivElement, ISkeletonProps>(
|
||||||
|
function Skeleton(
|
||||||
|
{
|
||||||
|
className,
|
||||||
|
variant = 'rounded',
|
||||||
|
children,
|
||||||
|
speed = 2,
|
||||||
|
startColor = 'bg-background-200',
|
||||||
|
isLoaded = false,
|
||||||
|
...props
|
||||||
|
},
|
||||||
|
ref
|
||||||
|
) {
|
||||||
|
if (!isLoaded) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
ref={ref}
|
||||||
|
className={`animate-pulse ${startColor} ${skeletonStyle({
|
||||||
|
variant,
|
||||||
|
speed,
|
||||||
|
class: className,
|
||||||
|
})}`}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return children;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
type ISkeletonTextProps = React.ComponentPropsWithoutRef<'div'> &
|
||||||
|
VariantProps<typeof skeletonTextStyle> & {
|
||||||
|
_lines?: number;
|
||||||
|
isLoaded?: boolean;
|
||||||
|
startColor?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const SkeletonText = React.forwardRef<HTMLDivElement, ISkeletonTextProps>(
|
||||||
|
function SkeletonText(
|
||||||
|
{
|
||||||
|
className,
|
||||||
|
_lines,
|
||||||
|
isLoaded = false,
|
||||||
|
startColor = 'bg-background-200',
|
||||||
|
gap = 2,
|
||||||
|
children,
|
||||||
|
...props
|
||||||
|
},
|
||||||
|
ref
|
||||||
|
) {
|
||||||
|
if (!isLoaded) {
|
||||||
|
if (_lines) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
ref={ref}
|
||||||
|
className={`flex flex-col ${skeletonTextStyle({
|
||||||
|
gap,
|
||||||
|
})}`}
|
||||||
|
>
|
||||||
|
{Array.from({ length: _lines }).map((_, index) => (
|
||||||
|
<div
|
||||||
|
key={index}
|
||||||
|
className={`animate-pulse ${startColor} ${skeletonTextStyle({
|
||||||
|
class: className,
|
||||||
|
})}`}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
ref={ref}
|
||||||
|
className={`animate-pulse ${startColor} ${skeletonTextStyle({
|
||||||
|
class: className,
|
||||||
|
})}`}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return children;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
Skeleton.displayName = 'Skeleton';
|
||||||
|
SkeletonText.displayName = 'SkeletonText';
|
||||||
|
|
||||||
|
export { Skeleton, SkeletonText };
|
||||||
35
components/ui/skeleton/styles.tsx
Normal file
35
components/ui/skeleton/styles.tsx
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
import { tva } from '@gluestack-ui/utils/nativewind-utils';
|
||||||
|
|
||||||
|
export const skeletonStyle = tva({
|
||||||
|
base: 'w-full h-full',
|
||||||
|
variants: {
|
||||||
|
variant: {
|
||||||
|
sharp: 'rounded-none',
|
||||||
|
circular: 'rounded-full',
|
||||||
|
rounded: 'rounded-md',
|
||||||
|
},
|
||||||
|
speed: {
|
||||||
|
1: 'duration-75',
|
||||||
|
2: 'duration-100',
|
||||||
|
3: 'duration-150',
|
||||||
|
4: 'duration-200',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
export const skeletonTextStyle = tva({
|
||||||
|
base: 'rounded-sm w-full',
|
||||||
|
variants: {
|
||||||
|
speed: {
|
||||||
|
1: 'duration-75',
|
||||||
|
2: 'duration-100',
|
||||||
|
3: 'duration-150',
|
||||||
|
4: 'duration-200',
|
||||||
|
},
|
||||||
|
gap: {
|
||||||
|
1: 'gap-1',
|
||||||
|
2: 'gap-2',
|
||||||
|
3: 'gap-3',
|
||||||
|
4: 'gap-4',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
240
components/ui/toast/index.tsx
Normal file
240
components/ui/toast/index.tsx
Normal file
@ -0,0 +1,240 @@
|
|||||||
|
'use client';
|
||||||
|
import React from 'react';
|
||||||
|
import { createToastHook } from '@gluestack-ui/core/toast/creator';
|
||||||
|
import { AccessibilityInfo, Text, View, ViewStyle } from 'react-native';
|
||||||
|
import { tva } from '@gluestack-ui/utils/nativewind-utils';
|
||||||
|
import { cssInterop } from 'nativewind';
|
||||||
|
import {
|
||||||
|
Motion,
|
||||||
|
AnimatePresence,
|
||||||
|
MotionComponentProps,
|
||||||
|
} from '@legendapp/motion';
|
||||||
|
import {
|
||||||
|
withStyleContext,
|
||||||
|
useStyleContext,
|
||||||
|
} from '@gluestack-ui/utils/nativewind-utils';
|
||||||
|
import type { VariantProps } from '@gluestack-ui/utils/nativewind-utils';
|
||||||
|
|
||||||
|
type IMotionViewProps = React.ComponentProps<typeof View> &
|
||||||
|
MotionComponentProps<typeof View, ViewStyle, unknown, unknown, unknown>;
|
||||||
|
|
||||||
|
const MotionView = Motion.View as React.ComponentType<IMotionViewProps>;
|
||||||
|
|
||||||
|
const useToast = createToastHook(MotionView, AnimatePresence);
|
||||||
|
const SCOPE = 'TOAST';
|
||||||
|
|
||||||
|
cssInterop(MotionView, { className: 'style' });
|
||||||
|
|
||||||
|
const toastStyle = tva({
|
||||||
|
base: 'p-4 m-1 rounded-md gap-1 web:pointer-events-auto shadow-hard-5 border-outline-100',
|
||||||
|
variants: {
|
||||||
|
action: {
|
||||||
|
error: 'bg-error-800',
|
||||||
|
warning: 'bg-warning-700',
|
||||||
|
success: 'bg-success-700',
|
||||||
|
info: 'bg-info-700',
|
||||||
|
muted: 'bg-background-800',
|
||||||
|
},
|
||||||
|
|
||||||
|
variant: {
|
||||||
|
solid: '',
|
||||||
|
outline: 'border bg-background-0',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const toastTitleStyle = tva({
|
||||||
|
base: 'text-typography-0 font-medium font-body tracking-md text-left',
|
||||||
|
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',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
parentVariants: {
|
||||||
|
variant: {
|
||||||
|
solid: '',
|
||||||
|
outline: '',
|
||||||
|
},
|
||||||
|
action: {
|
||||||
|
error: '',
|
||||||
|
warning: '',
|
||||||
|
success: '',
|
||||||
|
info: '',
|
||||||
|
muted: '',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
parentCompoundVariants: [
|
||||||
|
{
|
||||||
|
variant: 'outline',
|
||||||
|
action: 'error',
|
||||||
|
class: 'text-error-800',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
variant: 'outline',
|
||||||
|
action: 'warning',
|
||||||
|
class: 'text-warning-800',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
variant: 'outline',
|
||||||
|
action: 'success',
|
||||||
|
class: 'text-success-800',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
variant: 'outline',
|
||||||
|
action: 'info',
|
||||||
|
class: 'text-info-800',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
variant: 'outline',
|
||||||
|
action: 'muted',
|
||||||
|
class: 'text-background-800',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
const toastDescriptionStyle = tva({
|
||||||
|
base: 'font-normal font-body tracking-md text-left',
|
||||||
|
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',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
parentVariants: {
|
||||||
|
variant: {
|
||||||
|
solid: 'text-typography-50',
|
||||||
|
outline: 'text-typography-900',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const Root = withStyleContext(View, SCOPE);
|
||||||
|
type IToastProps = React.ComponentProps<typeof Root> & {
|
||||||
|
className?: string;
|
||||||
|
} & VariantProps<typeof toastStyle>;
|
||||||
|
|
||||||
|
const Toast = React.forwardRef<React.ComponentRef<typeof Root>, IToastProps>(
|
||||||
|
function Toast(
|
||||||
|
{ className, variant = 'solid', action = 'muted', ...props },
|
||||||
|
ref
|
||||||
|
) {
|
||||||
|
return (
|
||||||
|
<Root
|
||||||
|
ref={ref}
|
||||||
|
className={toastStyle({ variant, action, class: className })}
|
||||||
|
context={{ variant, action }}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
type IToastTitleProps = React.ComponentProps<typeof Text> & {
|
||||||
|
className?: string;
|
||||||
|
} & VariantProps<typeof toastTitleStyle>;
|
||||||
|
|
||||||
|
const ToastTitle = React.forwardRef<
|
||||||
|
React.ComponentRef<typeof Text>,
|
||||||
|
IToastTitleProps
|
||||||
|
>(function ToastTitle({ className, size = 'md', children, ...props }, ref) {
|
||||||
|
const { variant: parentVariant, action: parentAction } =
|
||||||
|
useStyleContext(SCOPE);
|
||||||
|
React.useEffect(() => {
|
||||||
|
// Issue from react-native side
|
||||||
|
// Hack for now, will fix this later
|
||||||
|
AccessibilityInfo.announceForAccessibility(children as string);
|
||||||
|
}, [children]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Text
|
||||||
|
{...props}
|
||||||
|
ref={ref}
|
||||||
|
aria-live="assertive"
|
||||||
|
aria-atomic="true"
|
||||||
|
role="alert"
|
||||||
|
className={toastTitleStyle({
|
||||||
|
size,
|
||||||
|
class: className,
|
||||||
|
parentVariants: {
|
||||||
|
variant: parentVariant,
|
||||||
|
action: parentAction,
|
||||||
|
},
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</Text>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
type IToastDescriptionProps = React.ComponentProps<typeof Text> & {
|
||||||
|
className?: string;
|
||||||
|
} & VariantProps<typeof toastDescriptionStyle>;
|
||||||
|
|
||||||
|
const ToastDescription = React.forwardRef<
|
||||||
|
React.ComponentRef<typeof Text>,
|
||||||
|
IToastDescriptionProps
|
||||||
|
>(function ToastDescription({ className, size = 'md', ...props }, ref) {
|
||||||
|
const { variant: parentVariant } = useStyleContext(SCOPE);
|
||||||
|
return (
|
||||||
|
<Text
|
||||||
|
ref={ref}
|
||||||
|
{...props}
|
||||||
|
className={toastDescriptionStyle({
|
||||||
|
size,
|
||||||
|
class: className,
|
||||||
|
parentVariants: {
|
||||||
|
variant: parentVariant,
|
||||||
|
},
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
Toast.displayName = 'Toast';
|
||||||
|
ToastTitle.displayName = 'ToastTitle';
|
||||||
|
ToastDescription.displayName = 'ToastDescription';
|
||||||
|
|
||||||
|
export { useToast, Toast, ToastTitle, ToastDescription };
|
||||||
48
components/user-header.tsx
Normal file
48
components/user-header.tsx
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
import { UserResponse } from "@/api/types";
|
||||||
|
import { router } from "expo-router";
|
||||||
|
import { StyleSheet, View } from "react-native";
|
||||||
|
import { Avatar, AvatarFallbackText } from "./ui/avatar";
|
||||||
|
import { Button, ButtonText } from "./ui/button";
|
||||||
|
import Panel from "./ui/panel";
|
||||||
|
import { ThemedText } from "./ui/themed-text";
|
||||||
|
interface UserHeaderProps {
|
||||||
|
user?: UserResponse
|
||||||
|
}
|
||||||
|
|
||||||
|
const UserHeader = ({ user }: UserHeaderProps) => {
|
||||||
|
return (
|
||||||
|
<Panel>
|
||||||
|
{user && (
|
||||||
|
<View className="flex-row">
|
||||||
|
<Avatar className="bg-indigo-600 me-3">
|
||||||
|
<AvatarFallbackText className="text-white">
|
||||||
|
{user.username}
|
||||||
|
</AvatarFallbackText>
|
||||||
|
</Avatar>
|
||||||
|
<View>
|
||||||
|
<ThemedText type="subtitle">Welcome, {user.username}</ThemedText>
|
||||||
|
<ThemedText>{user.type}</ThemedText>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
{!user && (
|
||||||
|
<>
|
||||||
|
<ThemedText type="subtitle">Hello, Guest</ThemedText>
|
||||||
|
<ThemedText style={styles.unathorizedText}>Please log in to proceed.</ThemedText>
|
||||||
|
<Button variant="solid" size="md" onPress={() => router.push("/login")}>
|
||||||
|
<ButtonText>Login</ButtonText>
|
||||||
|
</Button>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
</Panel>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const styles = StyleSheet.create({
|
||||||
|
unathorizedText: {
|
||||||
|
marginBottom: 10
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
export default UserHeader;
|
||||||
@ -8,10 +8,14 @@ import { Platform } from 'react-native';
|
|||||||
const tintColorLight = '#0a7ea4';
|
const tintColorLight = '#0a7ea4';
|
||||||
const tintColorDark = '#fff';
|
const tintColorDark = '#fff';
|
||||||
|
|
||||||
|
export const ContentPadding = 32;
|
||||||
|
|
||||||
export const Colors = {
|
export const Colors = {
|
||||||
light: {
|
light: {
|
||||||
text: '#11181C',
|
text: '#11181C',
|
||||||
background: '#fff',
|
background: '#fff',
|
||||||
|
defaultBackground: '#edededff',
|
||||||
|
errorText: "#cf0404",
|
||||||
tint: tintColorLight,
|
tint: tintColorLight,
|
||||||
icon: '#687076',
|
icon: '#687076',
|
||||||
tabIconDefault: '#687076',
|
tabIconDefault: '#687076',
|
||||||
@ -20,6 +24,8 @@ export const Colors = {
|
|||||||
dark: {
|
dark: {
|
||||||
text: '#ECEDEE',
|
text: '#ECEDEE',
|
||||||
background: '#151718',
|
background: '#151718',
|
||||||
|
defaultBackground: '#151515ff',
|
||||||
|
errorText: "#cf0404",
|
||||||
tint: tintColorDark,
|
tint: tintColorDark,
|
||||||
icon: '#9BA1A6',
|
icon: '#9BA1A6',
|
||||||
tabIconDefault: '#9BA1A6',
|
tabIconDefault: '#9BA1A6',
|
||||||
|
|||||||
3
global.css
Normal file
3
global.css
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
@tailwind base;
|
||||||
|
@tailwind components;
|
||||||
|
@tailwind utilities;
|
||||||
6
metro.config.js
Normal file
6
metro.config.js
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
const { getDefaultConfig } = require('expo/metro-config');
|
||||||
|
const { withNativeWind } = require('nativewind/metro');
|
||||||
|
|
||||||
|
const config = getDefaultConfig(__dirname);
|
||||||
|
|
||||||
|
module.exports = withNativeWind(config, { input: './global.css' });
|
||||||
1
nativewind-env.d.ts
vendored
Normal file
1
nativewind-env.d.ts
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
/// <reference types="nativewind/types" />
|
||||||
4449
package-lock.json
generated
4449
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
25
package.json
25
package.json
@ -11,7 +11,14 @@
|
|||||||
"lint": "expo lint"
|
"lint": "expo lint"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@expo/html-elements": "^0.10.1",
|
||||||
"@expo/vector-icons": "^15.0.3",
|
"@expo/vector-icons": "^15.0.3",
|
||||||
|
"@gluestack-ui/config": "^1.1.20",
|
||||||
|
"@gluestack-ui/core": "^3.0.12",
|
||||||
|
"@gluestack-ui/themed": "^1.1.73",
|
||||||
|
"@gluestack-ui/utils": "^3.0.13",
|
||||||
|
"@hookform/resolvers": "^5.2.2",
|
||||||
|
"@legendapp/motion": "^2.5.3",
|
||||||
"@react-navigation/bottom-tabs": "^7.4.0",
|
"@react-navigation/bottom-tabs": "^7.4.0",
|
||||||
"@react-navigation/elements": "^2.6.3",
|
"@react-navigation/elements": "^2.6.3",
|
||||||
"@react-navigation/native": "^7.1.8",
|
"@react-navigation/native": "^7.1.8",
|
||||||
@ -24,26 +31,38 @@
|
|||||||
"expo-image": "~3.0.11",
|
"expo-image": "~3.0.11",
|
||||||
"expo-linking": "~8.0.11",
|
"expo-linking": "~8.0.11",
|
||||||
"expo-router": "~6.0.21",
|
"expo-router": "~6.0.21",
|
||||||
|
"expo-secure-store": "~15.0.8",
|
||||||
"expo-splash-screen": "~31.0.13",
|
"expo-splash-screen": "~31.0.13",
|
||||||
"expo-status-bar": "~3.0.9",
|
"expo-status-bar": "~3.0.9",
|
||||||
"expo-symbols": "~1.0.8",
|
"expo-symbols": "~1.0.8",
|
||||||
"expo-system-ui": "~6.0.9",
|
"expo-system-ui": "~6.0.9",
|
||||||
"expo-web-browser": "~15.0.10",
|
"expo-web-browser": "~15.0.10",
|
||||||
|
"nativewind": "^4.2.1",
|
||||||
"react": "19.1.0",
|
"react": "19.1.0",
|
||||||
|
"react-aria": "^3.45.0",
|
||||||
"react-dom": "19.1.0",
|
"react-dom": "19.1.0",
|
||||||
|
"react-hook-form": "^7.69.0",
|
||||||
"react-native": "0.81.5",
|
"react-native": "0.81.5",
|
||||||
"react-native-gesture-handler": "~2.28.0",
|
"react-native-gesture-handler": "~2.28.0",
|
||||||
"react-native-reanimated": "~4.1.1",
|
"react-native-reanimated": "~4.1.0",
|
||||||
"react-native-safe-area-context": "~5.6.0",
|
"react-native-safe-area-context": "^5.6.2",
|
||||||
"react-native-screens": "~4.16.0",
|
"react-native-screens": "~4.16.0",
|
||||||
|
"react-native-svg": "^15.15.1",
|
||||||
|
"react-native-toast-notifications": "^3.4.0",
|
||||||
"react-native-vector-icons": "^10.3.0",
|
"react-native-vector-icons": "^10.3.0",
|
||||||
"react-native-web": "~0.21.0",
|
"react-native-web": "~0.21.0",
|
||||||
"react-native-worklets": "0.5.1"
|
"react-native-worklets": "^0.5.2",
|
||||||
|
"react-stately": "^3.43.0",
|
||||||
|
"tailwind-variants": "^0.1.20",
|
||||||
|
"zod": "^4.3.4"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/react": "~19.1.0",
|
"@types/react": "~19.1.0",
|
||||||
|
"babel-plugin-module-resolver": "^5.0.2",
|
||||||
"eslint": "^9.25.0",
|
"eslint": "^9.25.0",
|
||||||
"eslint-config-expo": "~10.0.0",
|
"eslint-config-expo": "~10.0.0",
|
||||||
|
"prettier-plugin-tailwindcss": "^0.5.14",
|
||||||
|
"tailwindcss": "^3.4.19",
|
||||||
"typescript": "~5.9.2"
|
"typescript": "~5.9.2"
|
||||||
},
|
},
|
||||||
"private": true
|
"private": true
|
||||||
|
|||||||
206
tailwind.config.js
Normal file
206
tailwind.config.js
Normal file
@ -0,0 +1,206 @@
|
|||||||
|
/** @type {import('tailwindcss').Config} */
|
||||||
|
module.exports = {
|
||||||
|
darkMode: process.env.DARK_MODE ? process.env.DARK_MODE : 'class',
|
||||||
|
content: [
|
||||||
|
'./app/**/*.{html,js,jsx,ts,tsx,mdx}',
|
||||||
|
'./components/**/*.{html,js,jsx,ts,tsx,mdx}',
|
||||||
|
'./utils/**/*.{html,js,jsx,ts,tsx,mdx}',
|
||||||
|
'./*.{html,js,jsx,ts,tsx,mdx}',
|
||||||
|
'./src/**/*.{html,js,jsx,ts,tsx,mdx}',
|
||||||
|
],
|
||||||
|
presets: [require('nativewind/preset')],
|
||||||
|
important: 'html',
|
||||||
|
safelist: [
|
||||||
|
{
|
||||||
|
pattern:
|
||||||
|
/(bg|border|text|stroke|fill)-(primary|secondary|tertiary|error|success|warning|info|typography|outline|background|indicator)-(0|50|100|200|300|400|500|600|700|800|900|950|white|gray|black|error|warning|muted|success|info|light|dark|primary)/,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
theme: {
|
||||||
|
extend: {
|
||||||
|
colors: {
|
||||||
|
primary: {
|
||||||
|
0: 'rgb(var(--color-primary-0)/<alpha-value>)',
|
||||||
|
50: 'rgb(var(--color-primary-50)/<alpha-value>)',
|
||||||
|
100: 'rgb(var(--color-primary-100)/<alpha-value>)',
|
||||||
|
200: 'rgb(var(--color-primary-200)/<alpha-value>)',
|
||||||
|
300: 'rgb(var(--color-primary-300)/<alpha-value>)',
|
||||||
|
400: 'rgb(var(--color-primary-400)/<alpha-value>)',
|
||||||
|
500: 'rgb(var(--color-primary-500)/<alpha-value>)',
|
||||||
|
600: 'rgb(var(--color-primary-600)/<alpha-value>)',
|
||||||
|
700: 'rgb(var(--color-primary-700)/<alpha-value>)',
|
||||||
|
800: 'rgb(var(--color-primary-800)/<alpha-value>)',
|
||||||
|
900: 'rgb(var(--color-primary-900)/<alpha-value>)',
|
||||||
|
950: 'rgb(var(--color-primary-950)/<alpha-value>)',
|
||||||
|
},
|
||||||
|
secondary: {
|
||||||
|
0: 'rgb(var(--color-secondary-0)/<alpha-value>)',
|
||||||
|
50: 'rgb(var(--color-secondary-50)/<alpha-value>)',
|
||||||
|
100: 'rgb(var(--color-secondary-100)/<alpha-value>)',
|
||||||
|
200: 'rgb(var(--color-secondary-200)/<alpha-value>)',
|
||||||
|
300: 'rgb(var(--color-secondary-300)/<alpha-value>)',
|
||||||
|
400: 'rgb(var(--color-secondary-400)/<alpha-value>)',
|
||||||
|
500: 'rgb(var(--color-secondary-500)/<alpha-value>)',
|
||||||
|
600: 'rgb(var(--color-secondary-600)/<alpha-value>)',
|
||||||
|
700: 'rgb(var(--color-secondary-700)/<alpha-value>)',
|
||||||
|
800: 'rgb(var(--color-secondary-800)/<alpha-value>)',
|
||||||
|
900: 'rgb(var(--color-secondary-900)/<alpha-value>)',
|
||||||
|
950: 'rgb(var(--color-secondary-950)/<alpha-value>)',
|
||||||
|
},
|
||||||
|
tertiary: {
|
||||||
|
50: 'rgb(var(--color-tertiary-50)/<alpha-value>)',
|
||||||
|
100: 'rgb(var(--color-tertiary-100)/<alpha-value>)',
|
||||||
|
200: 'rgb(var(--color-tertiary-200)/<alpha-value>)',
|
||||||
|
300: 'rgb(var(--color-tertiary-300)/<alpha-value>)',
|
||||||
|
400: 'rgb(var(--color-tertiary-400)/<alpha-value>)',
|
||||||
|
500: 'rgb(var(--color-tertiary-500)/<alpha-value>)',
|
||||||
|
600: 'rgb(var(--color-tertiary-600)/<alpha-value>)',
|
||||||
|
700: 'rgb(var(--color-tertiary-700)/<alpha-value>)',
|
||||||
|
800: 'rgb(var(--color-tertiary-800)/<alpha-value>)',
|
||||||
|
900: 'rgb(var(--color-tertiary-900)/<alpha-value>)',
|
||||||
|
950: 'rgb(var(--color-tertiary-950)/<alpha-value>)',
|
||||||
|
},
|
||||||
|
error: {
|
||||||
|
0: 'rgb(var(--color-error-0)/<alpha-value>)',
|
||||||
|
50: 'rgb(var(--color-error-50)/<alpha-value>)',
|
||||||
|
100: 'rgb(var(--color-error-100)/<alpha-value>)',
|
||||||
|
200: 'rgb(var(--color-error-200)/<alpha-value>)',
|
||||||
|
300: 'rgb(var(--color-error-300)/<alpha-value>)',
|
||||||
|
400: 'rgb(var(--color-error-400)/<alpha-value>)',
|
||||||
|
500: 'rgb(var(--color-error-500)/<alpha-value>)',
|
||||||
|
600: 'rgb(var(--color-error-600)/<alpha-value>)',
|
||||||
|
700: 'rgb(var(--color-error-700)/<alpha-value>)',
|
||||||
|
800: 'rgb(var(--color-error-800)/<alpha-value>)',
|
||||||
|
900: 'rgb(var(--color-error-900)/<alpha-value>)',
|
||||||
|
950: 'rgb(var(--color-error-950)/<alpha-value>)',
|
||||||
|
},
|
||||||
|
success: {
|
||||||
|
0: 'rgb(var(--color-success-0)/<alpha-value>)',
|
||||||
|
50: 'rgb(var(--color-success-50)/<alpha-value>)',
|
||||||
|
100: 'rgb(var(--color-success-100)/<alpha-value>)',
|
||||||
|
200: 'rgb(var(--color-success-200)/<alpha-value>)',
|
||||||
|
300: 'rgb(var(--color-success-300)/<alpha-value>)',
|
||||||
|
400: 'rgb(var(--color-success-400)/<alpha-value>)',
|
||||||
|
500: 'rgb(var(--color-success-500)/<alpha-value>)',
|
||||||
|
600: 'rgb(var(--color-success-600)/<alpha-value>)',
|
||||||
|
700: 'rgb(var(--color-success-700)/<alpha-value>)',
|
||||||
|
800: 'rgb(var(--color-success-800)/<alpha-value>)',
|
||||||
|
900: 'rgb(var(--color-success-900)/<alpha-value>)',
|
||||||
|
950: 'rgb(var(--color-success-950)/<alpha-value>)',
|
||||||
|
},
|
||||||
|
warning: {
|
||||||
|
0: 'rgb(var(--color-warning-0)/<alpha-value>)',
|
||||||
|
50: 'rgb(var(--color-warning-50)/<alpha-value>)',
|
||||||
|
100: 'rgb(var(--color-warning-100)/<alpha-value>)',
|
||||||
|
200: 'rgb(var(--color-warning-200)/<alpha-value>)',
|
||||||
|
300: 'rgb(var(--color-warning-300)/<alpha-value>)',
|
||||||
|
400: 'rgb(var(--color-warning-400)/<alpha-value>)',
|
||||||
|
500: 'rgb(var(--color-warning-500)/<alpha-value>)',
|
||||||
|
600: 'rgb(var(--color-warning-600)/<alpha-value>)',
|
||||||
|
700: 'rgb(var(--color-warning-700)/<alpha-value>)',
|
||||||
|
800: 'rgb(var(--color-warning-800)/<alpha-value>)',
|
||||||
|
900: 'rgb(var(--color-warning-900)/<alpha-value>)',
|
||||||
|
950: 'rgb(var(--color-warning-950)/<alpha-value>)',
|
||||||
|
},
|
||||||
|
info: {
|
||||||
|
0: 'rgb(var(--color-info-0)/<alpha-value>)',
|
||||||
|
50: 'rgb(var(--color-info-50)/<alpha-value>)',
|
||||||
|
100: 'rgb(var(--color-info-100)/<alpha-value>)',
|
||||||
|
200: 'rgb(var(--color-info-200)/<alpha-value>)',
|
||||||
|
300: 'rgb(var(--color-info-300)/<alpha-value>)',
|
||||||
|
400: 'rgb(var(--color-info-400)/<alpha-value>)',
|
||||||
|
500: 'rgb(var(--color-info-500)/<alpha-value>)',
|
||||||
|
600: 'rgb(var(--color-info-600)/<alpha-value>)',
|
||||||
|
700: 'rgb(var(--color-info-700)/<alpha-value>)',
|
||||||
|
800: 'rgb(var(--color-info-800)/<alpha-value>)',
|
||||||
|
900: 'rgb(var(--color-info-900)/<alpha-value>)',
|
||||||
|
950: 'rgb(var(--color-info-950)/<alpha-value>)',
|
||||||
|
},
|
||||||
|
typography: {
|
||||||
|
0: 'rgb(var(--color-typography-0)/<alpha-value>)',
|
||||||
|
50: 'rgb(var(--color-typography-50)/<alpha-value>)',
|
||||||
|
100: 'rgb(var(--color-typography-100)/<alpha-value>)',
|
||||||
|
200: 'rgb(var(--color-typography-200)/<alpha-value>)',
|
||||||
|
300: 'rgb(var(--color-typography-300)/<alpha-value>)',
|
||||||
|
400: 'rgb(var(--color-typography-400)/<alpha-value>)',
|
||||||
|
500: 'rgb(var(--color-typography-500)/<alpha-value>)',
|
||||||
|
600: 'rgb(var(--color-typography-600)/<alpha-value>)',
|
||||||
|
700: 'rgb(var(--color-typography-700)/<alpha-value>)',
|
||||||
|
800: 'rgb(var(--color-typography-800)/<alpha-value>)',
|
||||||
|
900: 'rgb(var(--color-typography-900)/<alpha-value>)',
|
||||||
|
950: 'rgb(var(--color-typography-950)/<alpha-value>)',
|
||||||
|
white: '#FFFFFF',
|
||||||
|
gray: '#D4D4D4',
|
||||||
|
black: '#181718',
|
||||||
|
},
|
||||||
|
outline: {
|
||||||
|
0: 'rgb(var(--color-outline-0)/<alpha-value>)',
|
||||||
|
50: 'rgb(var(--color-outline-50)/<alpha-value>)',
|
||||||
|
100: 'rgb(var(--color-outline-100)/<alpha-value>)',
|
||||||
|
200: 'rgb(var(--color-outline-200)/<alpha-value>)',
|
||||||
|
300: 'rgb(var(--color-outline-300)/<alpha-value>)',
|
||||||
|
400: 'rgb(var(--color-outline-400)/<alpha-value>)',
|
||||||
|
500: 'rgb(var(--color-outline-500)/<alpha-value>)',
|
||||||
|
600: 'rgb(var(--color-outline-600)/<alpha-value>)',
|
||||||
|
700: 'rgb(var(--color-outline-700)/<alpha-value>)',
|
||||||
|
800: 'rgb(var(--color-outline-800)/<alpha-value>)',
|
||||||
|
900: 'rgb(var(--color-outline-900)/<alpha-value>)',
|
||||||
|
950: 'rgb(var(--color-outline-950)/<alpha-value>)',
|
||||||
|
},
|
||||||
|
background: {
|
||||||
|
0: 'rgb(var(--color-background-0)/<alpha-value>)',
|
||||||
|
50: 'rgb(var(--color-background-50)/<alpha-value>)',
|
||||||
|
100: 'rgb(var(--color-background-100)/<alpha-value>)',
|
||||||
|
200: 'rgb(var(--color-background-200)/<alpha-value>)',
|
||||||
|
300: 'rgb(var(--color-background-300)/<alpha-value>)',
|
||||||
|
400: 'rgb(var(--color-background-400)/<alpha-value>)',
|
||||||
|
500: 'rgb(var(--color-background-500)/<alpha-value>)',
|
||||||
|
600: 'rgb(var(--color-background-600)/<alpha-value>)',
|
||||||
|
700: 'rgb(var(--color-background-700)/<alpha-value>)',
|
||||||
|
800: 'rgb(var(--color-background-800)/<alpha-value>)',
|
||||||
|
900: 'rgb(var(--color-background-900)/<alpha-value>)',
|
||||||
|
950: 'rgb(var(--color-background-950)/<alpha-value>)',
|
||||||
|
error: 'rgb(var(--color-background-error)/<alpha-value>)',
|
||||||
|
warning: 'rgb(var(--color-background-warning)/<alpha-value>)',
|
||||||
|
muted: 'rgb(var(--color-background-muted)/<alpha-value>)',
|
||||||
|
success: 'rgb(var(--color-background-success)/<alpha-value>)',
|
||||||
|
info: 'rgb(var(--color-background-info)/<alpha-value>)',
|
||||||
|
light: '#FBFBFB',
|
||||||
|
dark: '#181719',
|
||||||
|
},
|
||||||
|
indicator: {
|
||||||
|
primary: 'rgb(var(--color-indicator-primary)/<alpha-value>)',
|
||||||
|
info: 'rgb(var(--color-indicator-info)/<alpha-value>)',
|
||||||
|
error: 'rgb(var(--color-indicator-error)/<alpha-value>)',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
fontFamily: {
|
||||||
|
heading: undefined,
|
||||||
|
body: undefined,
|
||||||
|
mono: undefined,
|
||||||
|
jakarta: ['var(--font-plus-jakarta-sans)'],
|
||||||
|
roboto: ['var(--font-roboto)'],
|
||||||
|
code: ['var(--font-source-code-pro)'],
|
||||||
|
inter: ['var(--font-inter)'],
|
||||||
|
'space-mono': ['var(--font-space-mono)'],
|
||||||
|
},
|
||||||
|
fontWeight: {
|
||||||
|
extrablack: '950',
|
||||||
|
},
|
||||||
|
fontSize: {
|
||||||
|
'2xs': '10px',
|
||||||
|
},
|
||||||
|
boxShadow: {
|
||||||
|
'hard-1': '-2px 2px 8px 0px rgba(38, 38, 38, 0.20)',
|
||||||
|
'hard-2': '0px 3px 10px 0px rgba(38, 38, 38, 0.20)',
|
||||||
|
'hard-3': '2px 2px 8px 0px rgba(38, 38, 38, 0.20)',
|
||||||
|
'hard-4': '0px -3px 10px 0px rgba(38, 38, 38, 0.20)',
|
||||||
|
'hard-5': '0px 2px 10px 0px rgba(38, 38, 38, 0.10)',
|
||||||
|
'soft-1': '0px 0px 10px rgba(38, 38, 38, 0.1)',
|
||||||
|
'soft-2': '0px 0px 20px rgba(38, 38, 38, 0.2)',
|
||||||
|
'soft-3': '0px 0px 30px rgba(38, 38, 38, 0.1)',
|
||||||
|
'soft-4': '0px 0px 40px rgba(38, 38, 38, 0.1)',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
@ -5,6 +5,9 @@
|
|||||||
"paths": {
|
"paths": {
|
||||||
"@/*": [
|
"@/*": [
|
||||||
"./*"
|
"./*"
|
||||||
|
],
|
||||||
|
"tailwind.config": [
|
||||||
|
"./tailwind.config.js"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -12,6 +15,7 @@
|
|||||||
"**/*.ts",
|
"**/*.ts",
|
||||||
"**/*.tsx",
|
"**/*.tsx",
|
||||||
".expo/types/**/*.ts",
|
".expo/types/**/*.ts",
|
||||||
"expo-env.d.ts"
|
"expo-env.d.ts",
|
||||||
|
"nativewind-env.d.ts"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
15
utils/get-error-axios-message.ts
Normal file
15
utils/get-error-axios-message.ts
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
import { AxiosError } from "axios";
|
||||||
|
|
||||||
|
function getErrorAxiosMessage(error: unknown) {
|
||||||
|
console.error(error);
|
||||||
|
|
||||||
|
if (error instanceof AxiosError) {
|
||||||
|
let message = error.response?.data?.detail ||error.response?.data?.message || "Unknown Network Error";
|
||||||
|
|
||||||
|
return `${error.status || 500} - ${message}`;
|
||||||
|
} else {
|
||||||
|
return "Unknown Error"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default getErrorAxiosMessage;
|
||||||
38
utils/get-storage.ts
Normal file
38
utils/get-storage.ts
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
import * as SecureStore from 'expo-secure-store';
|
||||||
|
import { Platform } from 'react-native';
|
||||||
|
|
||||||
|
type StorageType = typeof Platform.OS;
|
||||||
|
type AvailableStorageType = "web"|"android"|"none";
|
||||||
|
|
||||||
|
interface useStorageFuncs {
|
||||||
|
getItemAsync: (key: string) => Promise<string | null>;
|
||||||
|
setItemAsync: (key: string, value: string) => Promise<void>;
|
||||||
|
removeItemAsync: (key: string) => Promise<void>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const storages: Record<AvailableStorageType, useStorageFuncs> = {
|
||||||
|
android: {
|
||||||
|
getItemAsync: async (token: string) => await SecureStore.getItemAsync(token),
|
||||||
|
setItemAsync: async (token: string, value: string) => await SecureStore.setItemAsync(token, value),
|
||||||
|
removeItemAsync: async (token: string) => await SecureStore.deleteItemAsync(token),
|
||||||
|
},
|
||||||
|
web: {
|
||||||
|
getItemAsync: async (token: string) => localStorage.getItem(token),
|
||||||
|
setItemAsync: async (token: string, value: string) => localStorage.setItem(token, value),
|
||||||
|
removeItemAsync: async (token: string) => localStorage.removeItem(token)
|
||||||
|
},
|
||||||
|
none: {
|
||||||
|
getItemAsync: async (token: string) => "not_realized",
|
||||||
|
setItemAsync: async (token: string, value: string) => { },
|
||||||
|
removeItemAsync: async (token: string) => { }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const getStorage = (type: StorageType = Platform.OS): useStorageFuncs => {
|
||||||
|
if(type in storages)
|
||||||
|
return storages[type as AvailableStorageType];
|
||||||
|
else
|
||||||
|
return storages.none;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default getStorage;
|
||||||
17
utils/token-storage.ts
Normal file
17
utils/token-storage.ts
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
import getStorage from '@/utils/get-storage';
|
||||||
|
|
||||||
|
const SECURE_KEY_ACCESS_TOKEN = "access_token";
|
||||||
|
|
||||||
|
const storage = getStorage();
|
||||||
|
|
||||||
|
export const getAuthToken = async () => {
|
||||||
|
return await storage.getItemAsync(SECURE_KEY_ACCESS_TOKEN);
|
||||||
|
}
|
||||||
|
|
||||||
|
export const deleteAuthToken = async () => {
|
||||||
|
await storage.removeItemAsync(SECURE_KEY_ACCESS_TOKEN);
|
||||||
|
}
|
||||||
|
|
||||||
|
export const setAuthToken = async (token: string) => {
|
||||||
|
await storage.setItemAsync(SECURE_KEY_ACCESS_TOKEN, token);
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user