add unistyles

This commit is contained in:
Aman Varshney
2025-05-07 14:29:11 +05:30
parent d09a284ce7
commit 6c269a4c5b
74 changed files with 1762 additions and 208 deletions

View File

@@ -0,0 +1,179 @@
import { authClient } from "@/lib/auth-client";
import { useQuery } from "@tanstack/react-query";
import { ScrollView, Text, TouchableOpacity, View } from "react-native";
import { StyleSheet } from "react-native-unistyles";
import { Container } from "@/components/container";
import { SignIn } from "@/components/sign-in";
import { SignUp } from "@/components/sign-up";
{{#if (eq api "orpc")}}
import { queryClient, orpc } from "@/utils/orpc";
{{/if}}
{{#if (eq api "trpc")}}
import { queryClient, trpc } from "@/utils/trpc";
{{/if}}
export default function Home() {
{{#if (eq api "orpc")}}
const healthCheck = useQuery(orpc.healthCheck.queryOptions());
const privateData = useQuery(orpc.privateData.queryOptions());
{{/if}}
{{#if (eq api "trpc")}}
const healthCheck = useQuery(trpc.healthCheck.queryOptions());
const privateData = useQuery(trpc.privateData.queryOptions());
{{/if}}
const { data: session } = authClient.useSession();
return (
<Container>
<ScrollView>
<View style={styles.pageContainer}>
<Text style={styles.headerTitle}>BETTER T STACK</Text>
{session?.user ? (
<View style={styles.sessionInfoCard}>
<View style={styles.sessionUserRow}>
<Text style={styles.welcomeText}>
Welcome,{" "}
<Text style={styles.userNameText}>{session.user.name}</Text>
</Text>
</View>
<Text style={styles.emailText}>{session.user.email}</Text>
<TouchableOpacity
style={styles.signOutButton}
onPress={() => {
authClient.signOut();
queryClient.invalidateQueries();
}}
>
<Text style={styles.signOutButtonText}>Sign Out</Text>
</TouchableOpacity>
</View>
) : null}
<View style={styles.apiStatusCard}>
<Text style={styles.cardTitle}>API Status</Text>
<View style={styles.apiStatusRow}>
<View
style={[
styles.statusIndicatorDot,
healthCheck.data
? styles.statusIndicatorGreen
: styles.statusIndicatorRed,
]}
/>
<Text style={styles.mutedText}>
{healthCheck.isLoading
? "Checking..."
: healthCheck.data
? "Connected to API"
: "API Disconnected"}
</Text>
</View>
</View>
<View style={styles.privateDataCard}>
<Text style={styles.cardTitle}>Private Data</Text>
{privateData && (
<View>
<Text style={styles.mutedText}>
{privateData.data?.message}
</Text>
</View>
)}
</View>
{!session?.user && (
<>
<SignIn />
<SignUp />
</>
)}
</View>
</ScrollView>
</Container>
);
}
const styles = StyleSheet.create((theme) => ({
pageContainer: {
paddingHorizontal: 8,
},
headerTitle: {
color: theme?.colors?.typography,
fontSize: 30,
fontWeight: "bold",
marginBottom: 16,
},
sessionInfoCard: {
marginBottom: 24,
padding: 16,
borderRadius: 8,
borderWidth: 1,
borderColor: theme?.colors?.border,
},
sessionUserRow: {
flexDirection: "row",
justifyContent: "space-between",
alignItems: "center",
marginBottom: 8,
},
welcomeText: {
color: theme?.colors?.typography,
fontSize: 16,
},
userNameText: {
fontWeight: "500",
color: theme?.colors?.typography,
},
emailText: {
color: theme?.colors?.typography,
fontSize: 14,
marginBottom: 16,
},
signOutButton: {
backgroundColor: theme?.colors?.destructive,
paddingVertical: 8,
paddingHorizontal: 16,
borderRadius: 6,
alignSelf: "flex-start",
},
signOutButtonText: {
fontWeight: "500",
},
apiStatusCard: {
marginBottom: 24,
borderRadius: 8,
borderWidth: 1,
borderColor: theme?.colors?.border,
padding: 16,
},
cardTitle: {
marginBottom: 12,
fontWeight: "500",
color: theme?.colors?.typography,
},
apiStatusRow: {
flexDirection: "row",
alignItems: "center",
gap: 8,
},
statusIndicatorDot: {
height: 12,
width: 12,
borderRadius: 9999,
},
statusIndicatorGreen: {
backgroundColor: theme.colors.success,
},
statusIndicatorRed: {
backgroundColor: theme.colors.destructive,
},
mutedText: {
color: theme?.colors?.typography,
},
privateDataCard: {
marginBottom: 24,
borderRadius: 8,
borderWidth: 1,
borderColor: theme?.colors?.border,
padding: 16,
},
}));

View File

@@ -0,0 +1,134 @@
import { authClient } from "@/lib/auth-client";
{{#if (eq api "trpc")}}
import { queryClient } from "@/utils/trpc";
{{/if}}
{{#if (eq api "orpc")}}
import { queryClient } from "@/utils/orpc";
{{/if}}
import { useState } from "react";
import {
ActivityIndicator,
Text,
TextInput,
TouchableOpacity,
View,
} from "react-native";
import { StyleSheet } from "react-native-unistyles";
export function SignIn() {
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const handleLogin = async () => {
setIsLoading(true);
setError(null);
await authClient.signIn.email(
{
email,
password,
},
{
onError: (error) => {
setError(error.error?.message || "Failed to sign in");
setIsLoading(false);
},
onSuccess: () => {
setEmail("");
setPassword("");
queryClient.refetchQueries();
},
onFinished: () => {
setIsLoading(false);
},
},
);
};
return (
<View style={styles.container}>
<Text style={styles.title}>Sign In</Text>
{error && (
<View style={styles.errorContainer}>
<Text style={styles.errorText}>{error}</Text>
</View>
)}
<TextInput
style={styles.input}
placeholder="Email"
value={email}
onChangeText={setEmail}
keyboardType="email-address"
autoCapitalize="none"
/>
<TextInput
style={styles.input}
placeholder="Password"
value={password}
onChangeText={setPassword}
secureTextEntry
/>
<TouchableOpacity
onPress={handleLogin}
disabled={isLoading}
style={styles.button}
>
{isLoading ? (
<ActivityIndicator size="small" color="#fff" />
) : (
<Text style={styles.buttonText}>Sign In</Text>
)}
</TouchableOpacity>
</View>
);
}
const styles = StyleSheet.create((theme) => ({
container: {
marginTop: 24,
padding: 16,
borderRadius: 8,
borderWidth: 1,
borderColor: theme.colors.border,
},
title: {
fontSize: 18,
fontWeight: "600",
color: theme.colors.typography,
marginBottom: 16,
},
errorContainer: {
marginBottom: 16,
padding: 12,
borderRadius: 6,
},
errorText: {
color: theme.colors.destructive,
fontSize: 14,
},
input: {
marginBottom: 12,
padding: 16,
borderRadius: 6,
color: theme.colors.typography,
borderWidth: 1,
borderColor: theme.colors.border,
},
button: {
backgroundColor: theme.colors.primary,
padding: 16,
borderRadius: 6,
flexDirection: "row",
justifyContent: "center",
alignItems: "center",
},
buttonText: {
fontWeight: "500",
},
}));

View File

@@ -0,0 +1,152 @@
import { authClient } from "@/lib/auth-client";
{{#if (eq api "trpc")}}
import { queryClient } from "@/utils/trpc";
{{/if}}
{{#if (eq api "orpc")}}
import { queryClient } from "@/utils/orpc";
{{/if}}
import { useState } from "react";
import {
ActivityIndicator,
Text,
TextInput,
TouchableOpacity,
View,
} from "react-native";
import { StyleSheet } from "react-native-unistyles";
export function SignUp() {
const [name, setName] = useState("");
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const handleSignUp = async () => {
setIsLoading(true);
setError(null);
await authClient.signUp.email(
{
name,
email,
password,
},
{
onError: (error) => {
setError(error.error?.message || "Failed to sign up");
setIsLoading(false);
},
onSuccess: () => {
setName("");
setEmail("");
setPassword("");
queryClient.refetchQueries();
},
onFinished: () => {
setIsLoading(false);
},
},
);
};
return (
<View style={styles.container}>
<Text style={styles.title}>Create Account</Text>
{error && (
<View style={styles.errorContainer}>
<Text style={styles.errorText}>{error}</Text>
</View>
)}
<TextInput
style={styles.input}
placeholder="Name"
value={name}
onChangeText={setName}
/>
<TextInput
style={styles.input}
placeholder="Email"
value={email}
onChangeText={setEmail}
keyboardType="email-address"
autoCapitalize="none"
/>
<TextInput
style={styles.inputLast}
placeholder="Password"
value={password}
onChangeText={setPassword}
secureTextEntry
/>
<TouchableOpacity
onPress={handleSignUp}
disabled={isLoading}
style={styles.button}
>
{isLoading ? (
<ActivityIndicator size="small" color="#fff" />
) : (
<Text style={styles.buttonText}>Sign Up</Text>
)}
</TouchableOpacity>
</View>
);
}
const styles = StyleSheet.create((theme) => ({
container: {
marginTop: 24,
padding: 16,
borderRadius: 8,
borderWidth: 1,
borderColor: theme.colors.border,
},
title: {
fontSize: 18,
fontWeight: "600",
color: theme.colors.typography,
marginBottom: 16,
},
errorContainer: {
marginBottom: 16,
padding: 12,
borderRadius: 6,
},
errorText: {
color: theme.colors.destructive,
fontSize: 14,
},
input: {
marginBottom: 12,
padding: 16,
borderRadius: 6,
color: theme.colors.typography,
borderWidth: 1,
borderColor: theme.colors.border,
},
inputLast: {
marginBottom: 16,
padding: 16,
borderRadius: 6,
color: theme.colors.typography,
borderWidth: 1,
borderColor: theme.colors.border,
},
button: {
backgroundColor: theme.colors.primary,
padding: 16,
borderRadius: 6,
flexDirection: "row",
justifyContent: "center",
alignItems: "center",
},
buttonText: {
fontWeight: "500",
},
}));

View File

Before

Width:  |  Height:  |  Size: 17 KiB

After

Width:  |  Height:  |  Size: 17 KiB

View File

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 22 KiB

View File

Before

Width:  |  Height:  |  Size: 46 KiB

After

Width:  |  Height:  |  Size: 46 KiB

View File

@@ -0,0 +1,24 @@
node_modules/
.expo/
dist/
npm-debug.*
*.jks
*.p8
*.p12
*.key
*.mobileprovision
*.orig.*
web-build/
# expo router
expo-env.d.ts
ios
android
# macOS
.DS_Store
# Temporary files created by Metro to check the health of the file watcher
.metro-health-check*

View File

@@ -0,0 +1,44 @@
{
"expo": {
"name": "my-better-t-app",
"slug": "my-better-t-app",
"version": "1.0.0",
"newArchEnabled": true,
"scheme": "my-better-t-app",
"web": {
"bundler": "metro",
"output": "static",
"favicon": "./assets/favicon.png"
},
"plugins": [
"expo-router",
"react-native-edge-to-edge",
"expo-secure-store"
],
"experiments": {
"typedRoutes": true,
"tsconfigPaths": true,
"reactCompiler": true
},
"orientation": "portrait",
"icon": "./assets/icon.png",
"userInterfaceStyle": "automatic",
"splash": {
"image": "./assets/splash.png",
"resizeMode": "contain",
"backgroundColor": "#ffffff"
},
"assetBundlePatterns": ["**/*"],
"ios": {
"supportsTablet": true,
"bundleIdentifier": "com.amanvarshney01.mybettertapp"
},
"android": {
"adaptiveIcon": {
"foregroundImage": "./assets/adaptive-icon.png",
"backgroundColor": "#ffffff"
},
"package": "com.amanvarshney01.mybettertapp"
}
}
}

View File

@@ -0,0 +1,34 @@
import { Tabs } from "expo-router";
import { useUnistyles } from "react-native-unistyles";
import { TabBarIcon } from "@/components/tabbar-icon";
export default function TabLayout() {
const { theme } = useUnistyles();
return (
<Tabs
screenOptions={{
headerShown: false,
tabBarStyle: {
backgroundColor: theme.colors.background,
},
}}
>
<Tabs.Screen
name="index"
options={{
title: "Tab One",
tabBarIcon: ({ color }) => <TabBarIcon name="code" color={color} />,
}}
/>
<Tabs.Screen
name="two"
options={{
title: "Tab Two",
tabBarIcon: ({ color }) => <TabBarIcon name="code" color={color} />,
}}
/>
</Tabs>
);
}

View File

@@ -0,0 +1,29 @@
import { Stack } from "expo-router";
import { StyleSheet } from "react-native-unistyles";
import { Container } from "@/components/container";
import { Text, View } from "react-native";
export default function Home() {
return (
<>
<Stack.Screen options={{ title: "Tab One" }} />
<Container>
<View style={styles.container}>
<Text style={styles.text}>Tab One</Text>
</View>
</Container>
</>
);
}
const styles = StyleSheet.create((theme) => ({
text: {
color: theme.colors.typography,
},
container: {
flex: 1,
paddingBottom: 100,
justifyContent: "center",
alignItems: "center",
},
}));

View File

@@ -0,0 +1,29 @@
import { Stack } from "expo-router";
import { StyleSheet } from "react-native-unistyles";
import { Container } from "@/components/container";
import { Text, View } from "react-native";
export default function Home() {
return (
<>
<Stack.Screen options={{ title: "Tab Two" }} />
<Container>
<View style={styles.container}>
<Text style={styles.text}>Tab Two</Text>
</View>
</Container>
</>
);
}
const styles = StyleSheet.create((theme) => ({
text: {
color: theme.colors.typography,
},
container: {
flex: 1,
paddingBottom: 100,
justifyContent: "center",
alignItems: "center",
},
}));

View File

@@ -0,0 +1,59 @@
import { Ionicons, MaterialIcons } from "@expo/vector-icons";
import { Link } from "expo-router";
import { Drawer } from "expo-router/drawer";
import { useUnistyles } from "react-native-unistyles";
import { HeaderButton } from "../../components/header-button";
const DrawerLayout = () => {
const { theme } = useUnistyles();
return (
<Drawer
screenOptions={{
headerStyle: {
backgroundColor: theme.colors.background,
},
headerTitleStyle: {
color: theme.colors.typography,
},
headerTintColor: theme.colors.typography,
drawerStyle: {
backgroundColor: theme.colors.background,
},
drawerLabelStyle: {
color: theme.colors.typography,
},
drawerInactiveTintColor: theme.colors.typography,
}}
>
<Drawer.Screen
name="index"
options={{
headerTitle: "Home",
drawerLabel: "Home",
drawerIcon: ({ size, color }) => (
<Ionicons name="home-outline" size={size} color={color} />
),
}}
/>
<Drawer.Screen
name="(tabs)"
options={{
headerTitle: "Tabs",
drawerLabel: "Tabs",
drawerIcon: ({ size, color }) => (
<MaterialIcons name="border-bottom" size={size} color={color} />
),
headerRight: () => (
<Link href="/modal" asChild>
<HeaderButton />
</Link>
),
}}
/>
</Drawer>
);
};
export default DrawerLayout;

View File

@@ -0,0 +1,115 @@
import { ScrollView, Text, View } from "react-native";
import { StyleSheet } from "react-native-unistyles";
import { Container } from "@/components/container";
{{#if (eq api "orpc")}}
import { useQuery } from "@tanstack/react-query";
import { orpc } from "@/utils/orpc";
{{/if}}
{{#if (eq api "trpc")}}
import { useQuery } from "@tanstack/react-query";
import { trpc } from "@/utils/trpc";
{{/if}}
{{#if (eq backend "convex")}}
import { useQuery } from "convex/react";
import { api } from "@{{ projectName }}/backend/convex/_generated/api.js";
{{/if}}
export default function Home() {
{{#if (eq api "orpc")}}
const healthCheck = useQuery(orpc.healthCheck.queryOptions());
{{/if}}
{{#if (eq api "trpc")}}
const healthCheck = useQuery(trpc.healthCheck.queryOptions());
{{/if}}
{{#if (eq backend "convex")}}
const healthCheck = useQuery(api.healthCheck.get);
{{/if}}
return (
<Container>
<ScrollView contentContainerStyle={styles.pageContainer}>
<Text style={styles.headerTitle}>BETTER T STACK</Text>
<View style={styles.apiStatusCard}>
<Text style={styles.cardTitle}>API Status</Text>
<View style={styles.apiStatusRow}>
<View
style={[
styles.statusIndicatorDot,
{{#if (or (eq api "orpc") (eq api "trpc"))}}
healthCheck.data
? styles.statusIndicatorGreen
: styles.statusIndicatorRed,
{{else}}
healthCheck === "OK"
? styles.statusIndicatorGreen
: styles.statusIndicatorRed,
{{/if}}
]}
/>
<Text style={styles.statusText}>
{{#if (or (eq api "orpc") (eq api "trpc"))}}
{healthCheck.isLoading
? "Checking..."
: healthCheck.data
? "Connected"
: "Disconnected"}
{{/if}}
{{#if (eq backend "convex")}}
{healthCheck === undefined
? "Checking..."
: healthCheck === "OK"
? "Connected"
: "Error"}
{{/if}}
</Text>
</View>
</View>
</ScrollView>
</Container>
);
}
const styles = StyleSheet.create((theme) => ({
pageContainer: {
paddingHorizontal: 8,
},
headerTitle: {
color: theme?.colors?.typography,
fontSize: 30,
fontWeight: "bold",
marginBottom: 16,
},
apiStatusCard: {
marginBottom: 24,
borderRadius: 8,
borderWidth: 1,
borderColor: theme?.colors?.border,
padding: 16,
},
cardTitle: {
marginBottom: 12,
fontWeight: "500",
color: theme?.colors?.typography,
},
apiStatusRow: {
flexDirection: "row",
alignItems: "center",
gap: 8,
},
statusIndicatorDot: {
height: 12,
width: 12,
borderRadius: 9999,
},
statusIndicatorGreen: {
backgroundColor: theme.colors.success,
},
statusIndicatorRed: {
backgroundColor: theme.colors.destructive,
},
statusText: {
color: theme?.colors?.typography,
},
}));

View File

@@ -0,0 +1,48 @@
import { ScrollViewStyleReset } from 'expo-router/html';
import '../unistyles';
// This file is web-only and used to configure the root HTML for every
// web page during static rendering.
// The contents of this function only run in Node.js environments and
// do not have access to the DOM or browser APIs.
export default function Root({ children }: { children: React.ReactNode }) {
return (
<html lang="en">
<head>
<meta charSet="utf-8" />
<meta httpEquiv="X-UA-Compatible" content="IE=edge" />
{/*
This viewport disables scaling which makes the mobile website act more like a native app.
However this does reduce built-in accessibility. If you want to enable scaling, use this instead:
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover" />
*/}
<meta
name="viewport"
content="width=device-width,initial-scale=1,minimum-scale=1,maximum-scale=1.00001,viewport-fit=cover"
/>
{/*
Disable body scrolling on web. This makes ScrollView components work closer to how they do on native.
However, body scrolling is often nice to have for mobile web. If you want to enable it, remove this line.
*/}
<ScrollViewStyleReset />
{/* Using raw CSS styles as an escape-hatch to ensure the background color never flickers in dark-mode. */}
<style dangerouslySetInnerHTML={{ __html: responsiveBackground }} />
{/* Add any additional <head> elements that you want globally available on web... */}
</head>
<body>{children}</body>
</html>
);
}
const responsiveBackground = `
body {
background-color: #fff;
}
@media (prefers-color-scheme: dark) {
body {
background-color: #000;
}
}`;

View File

@@ -0,0 +1,34 @@
import { Link, Stack } from "expo-router";
import { Text } from "react-native";
import { StyleSheet } from "react-native-unistyles";
import { Container } from "@/components/container";
export default function NotFoundScreen() {
return (
<>
<Stack.Screen options={{ title: "Oops!" }} />
<Container>
<Text style={styles.title}>This screen doesn't exist.</Text>
<Link href="/" style={styles.link}>
<Text style={styles.linkText}>Go to home screen!</Text>
</Link>
</Container>
</>
);
}
const styles = StyleSheet.create((theme) => ({
title: {
fontSize: 20,
fontWeight: "bold",
color: theme.colors.typography,
},
link: {
marginTop: 16,
paddingVertical: 16,
},
linkText: {
fontSize: 14,
},
}));

View File

@@ -0,0 +1,77 @@
{{#if (eq api "trpc")}}
import { queryClient } from "@/utils/trpc";
{{/if}}
{{#if (eq api "orpc")}}
import { queryClient } from "@/utils/orpc";
{{/if}}
{{#if (eq backend "convex")}}
import { ConvexProvider, ConvexReactClient } from "convex/react";
{{else}}
import { QueryClientProvider } from "@tanstack/react-query";
{{/if}}
import { Stack } from "expo-router";
import { GestureHandlerRootView } from "react-native-gesture-handler";
import { useUnistyles } from "react-native-unistyles";
export const unstable_settings = {
// Ensure that reloading on `/modal` keeps a back button present.
initialRouteName: "(drawer)",
};
{{#if (eq backend "convex")}}
const convex = new ConvexReactClient(process.env.EXPO_PUBLIC_CONVEX_URL!, {
unsavedChangesWarning: false,
});
{{/if}}
export default function RootLayout() {
const { theme } = useUnistyles();
return (
{{#if (eq backend "convex")}}
<ConvexProvider client={convex}>
<GestureHandlerRootView style=\{{ flex: 1 }}>
<Stack
screenOptions=\{{
headerStyle: {
backgroundColor: theme.colors.background,
},
headerTitleStyle: {
color: theme.colors.typography,
},
headerTintColor: theme.colors.typography,
}}
>
<Stack.Screen name="(drawer)" options=\{{ headerShown: false }} />
<Stack.Screen
name="modal"
options=\{{ title: "Modal", presentation: "modal" }}
/>
</Stack>
</GestureHandlerRootView>
</ConvexProvider>
{{else}}
<QueryClientProvider client={queryClient}>
<GestureHandlerRootView style=\{{ flex: 1 }}>
<Stack
screenOptions=\{{
headerStyle: {
backgroundColor: theme.colors.background,
},
headerTitleStyle: {
color: theme.colors.typography,
},
headerTintColor: theme.colors.typography,
}}
>
<Stack.Screen name="(drawer)" options=\{{ headerShown: false }} />
<Stack.Screen
name="modal"
options=\{{ title: "Modal", presentation: "modal" }}
/>
</Stack>
</GestureHandlerRootView>
</QueryClientProvider>
{{/if}}
);
}

View File

@@ -0,0 +1,29 @@
import { Container } from "@/components/container";
import { StatusBar } from "expo-status-bar";
import { Platform, Text, View } from "react-native";
import { StyleSheet } from "react-native-unistyles";
export default function Modal() {
return (
<>
<StatusBar style={Platform.OS === "ios" ? "light" : "auto"} />
<Container>
<View style={styles.container}>
<Text style={styles.text}>Model</Text>
</View>
</Container>
</>
);
}
const styles = StyleSheet.create((theme) => ({
text: {
color: theme.colors.typography,
},
container: {
flex: 1,
paddingBottom: 100,
justifyContent: "center",
alignItems: "center",
},
}));

View File

@@ -0,0 +1,20 @@
module.exports = function (api) {
api.cache(true);
const plugins = [];
plugins.push([
'react-native-unistyles/plugin',
{
autoProcessRoot: 'app',
autoProcessImports: ['@/components'],
},
]);
plugins.push('react-native-reanimated/plugin');
return {
presets: ['babel-preset-expo'],
plugins,
};
};

View File

@@ -0,0 +1,9 @@
export const breakpoints = {
xs: 0,
sm: 576,
md: 768,
lg: 992,
xl: 1200,
superLarge: 2000,
tvLike: 4000,
} as const;

View File

@@ -0,0 +1,20 @@
import React from "react";
import { View } from "react-native";
import { StyleSheet } from "react-native-unistyles";
export const Container = ({ children }: { children: React.ReactNode }) => {
return <View style={styles.container}>{children}</View>;
};
const styles = StyleSheet.create((theme, rt) => ({
container: {
flex: 1,
paddingBottom: rt.insets.bottom,
backgroundColor: theme.colors.background,
transform: [
{
translateY: rt.insets.ime * -1,
},
],
},
}));

View File

@@ -0,0 +1,31 @@
import FontAwesome from '@expo/vector-icons/FontAwesome';
import { forwardRef } from 'react';
import { Pressable, StyleSheet } from 'react-native';
export const HeaderButton = forwardRef<typeof Pressable, { onPress?: () => void }>(
({ onPress }, ref) => {
return (
<Pressable onPress={onPress}>
{({ pressed }) => (
<FontAwesome
name="info-circle"
size={25}
color="gray"
style={[
styles.headerRight,
{
opacity: pressed ? 0.5 : 1,
},
]}
/>
)}
</Pressable>
);
}
);
export const styles = StyleSheet.create({
headerRight: {
marginRight: 15,
},
});

View File

@@ -0,0 +1,15 @@
import FontAwesome from '@expo/vector-icons/FontAwesome';
import { StyleSheet } from 'react-native';
export const TabBarIcon = (props: {
name: React.ComponentProps<typeof FontAwesome>['name'];
color: string;
}) => {
return <FontAwesome size={28} style={styles.tabBarIcon} {...props} />;
};
export const styles = StyleSheet.create({
tabBarIcon: {
marginBottom: -3,
},
});

View File

@@ -0,0 +1,3 @@
/// <reference types="expo/types" />
// NOTE: This file should not be edited and should be in your git ignore

View File

@@ -0,0 +1,2 @@
import 'expo-router/entry';
import './unistyles';

View File

@@ -0,0 +1,20 @@
// Learn more https://docs.expo.io/guides/customizing-metro
const { getDefaultConfig } = require("expo/metro-config");
const path = require("path");
const workspaceRoot = path.resolve(__dirname, "../..");
const projectRoot = __dirname;
const config = getDefaultConfig(projectRoot);
// 1. Watch all files within the monorepo
config.watchFolders = [workspaceRoot];
// 2. Let Metro know where to resolve packages, and in what order
config.resolver.nodeModulesPaths = [
path.resolve(projectRoot, "node_modules"),
path.resolve(workspaceRoot, "node_modules"),
];
// 3. Force Metro to resolve (sub)dependencies only from the `nodeModulesPaths`
config.resolver.disableHierarchicalLookup = true;
module.exports = config;

View File

@@ -0,0 +1,47 @@
{
"name": "native",
"version": "1.0.0",
"main": "index.js",
"scripts": {
"dev": "expo start --clear",
"android": "expo run:android",
"ios": "expo run:ios",
"web": "expo start --web"
},
"dependencies": {
"@better-auth/expo": "^1.2.7",
"@expo/vector-icons": "^14.0.0",
"@react-navigation/bottom-tabs": "^7.0.5",
"@react-navigation/drawer": "^7.0.0",
"@react-navigation/native": "^7.0.3",
"@tanstack/react-form": "^1.0.5",
"babel-plugin-react-compiler": "^19.0.0-beta-af1b7da-20250417",
"expo": "^53.0.8",
"expo-constants": "~17.1.4",
"expo-linking": "~7.1.4",
"expo-router": "~5.0.3",
"expo-secure-store": "~14.2.3",
"expo-status-bar": "~2.2.3",
"expo-system-ui": "~5.0.6",
"expo-dev-client": "~5.1.8",
"expo-web-browser": "~14.1.6",
"react": "19.0.0",
"react-dom": "19.0.0",
"react-native": "0.79.2",
"react-native-edge-to-edge": "1.6.0",
"react-native-gesture-handler": "~2.24.0",
"react-native-nitro-modules": "0.25.2",
"react-native-reanimated": "~3.17.4",
"react-native-safe-area-context": "5.4.0",
"react-native-screens": "~4.10.0",
"react-native-unistyles": "3.0.0-rc.3",
"react-native-web": "^0.20.0"
},
"devDependencies": {
"ajv": "^8.12.0",
"@babel/core": "^7.20.0",
"@types/react": "~19.0.10",
"typescript": "~5.8.3"
},
"private": true
}

View File

@@ -0,0 +1,35 @@
const sharedColors = {
success: "#22C55E",
destructive: "#DC2626",
border: "#D1D5DB",
} as const;
export const lightTheme = {
colors: {
...sharedColors,
typography: "#000000",
background: "#ffffff",
primary: "#3B82F6",
},
margins: {
sm: 2,
md: 4,
lg: 8,
xl: 12,
},
} as const;
export const darkTheme = {
colors: {
...sharedColors,
typography: "#ffffff",
background: "#000000",
primary: "#60A5FA",
},
margins: {
sm: 2,
md: 4,
lg: 8,
xl: 12,
},
} as const;

View File

@@ -0,0 +1,12 @@
{
"extends": "expo/tsconfig.base",
"compilerOptions": {
"strict": true,
"jsx": "react-jsx",
"baseUrl": ".",
"paths": {
"@/*": ["*"]
}
},
"include": ["**/*.ts", "**/*.tsx", ".expo/types/**/*.ts", "expo-env.d.ts"]
}

View File

@@ -0,0 +1,27 @@
import { StyleSheet } from 'react-native-unistyles';
import { breakpoints } from './breakpoints';
import { lightTheme, darkTheme } from './theme';
type AppBreakpoints = typeof breakpoints;
type AppThemes = {
light: typeof lightTheme;
dark: typeof darkTheme;
};
declare module 'react-native-unistyles' {
export interface UnistylesBreakpoints extends AppBreakpoints {}
export interface UnistylesThemes extends AppThemes {}
}
StyleSheet.configure({
breakpoints,
themes: {
light: lightTheme,
dark: darkTheme,
},
settings: {
adaptiveThemes: true,
},
});

View File

@@ -41,10 +41,10 @@ export default function Header() {
);
{{else if (includes frontend "react-router")}}
return (
<NavLink
key={to}
to={to}
className={({ isActive }) => isActive ? "font-bold" : ""}
<NavLink
key={to}
to={to}
className={({ isActive }) => isActive ? "font-bold" : ""}
end
>
{label}
@@ -60,7 +60,6 @@ export default function Header() {
</Link>
);
{{else}}
// Fallback case (shouldn't happen with valid frontend selection)
return null;
{{/if}}
})}
@@ -77,4 +76,4 @@ export default function Header() {
<hr />
</div>
);
}
}