add unistyles
@@ -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,
|
||||
},
|
||||
}));
|
||||
@@ -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",
|
||||
},
|
||||
}));
|
||||
@@ -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",
|
||||
},
|
||||
}));
|
||||
|
Before Width: | Height: | Size: 17 KiB After Width: | Height: | Size: 17 KiB |
|
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 1.4 KiB |
|
Before Width: | Height: | Size: 22 KiB After Width: | Height: | Size: 22 KiB |
|
Before Width: | Height: | Size: 46 KiB After Width: | Height: | Size: 46 KiB |
24
apps/cli/templates/frontend/native/unistyles/_gitignore
Normal 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*
|
||||
44
apps/cli/templates/frontend/native/unistyles/app.json
Normal 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"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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",
|
||||
},
|
||||
}));
|
||||
@@ -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",
|
||||
},
|
||||
}));
|
||||
@@ -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;
|
||||
@@ -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,
|
||||
},
|
||||
}));
|
||||
48
apps/cli/templates/frontend/native/unistyles/app/+html.tsx
Normal 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;
|
||||
}
|
||||
}`;
|
||||
@@ -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,
|
||||
},
|
||||
}));
|
||||
@@ -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}}
|
||||
);
|
||||
}
|
||||
29
apps/cli/templates/frontend/native/unistyles/app/modal.tsx
Normal 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",
|
||||
},
|
||||
}));
|
||||
20
apps/cli/templates/frontend/native/unistyles/babel.config.js
Normal 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,
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,9 @@
|
||||
export const breakpoints = {
|
||||
xs: 0,
|
||||
sm: 576,
|
||||
md: 768,
|
||||
lg: 992,
|
||||
xl: 1200,
|
||||
superLarge: 2000,
|
||||
tvLike: 4000,
|
||||
} as const;
|
||||
@@ -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,
|
||||
},
|
||||
],
|
||||
},
|
||||
}));
|
||||
@@ -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,
|
||||
},
|
||||
});
|
||||
@@ -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,
|
||||
},
|
||||
});
|
||||
3
apps/cli/templates/frontend/native/unistyles/expo-env.d.ts
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
/// <reference types="expo/types" />
|
||||
|
||||
// NOTE: This file should not be edited and should be in your git ignore
|
||||
2
apps/cli/templates/frontend/native/unistyles/index.js
Normal file
@@ -0,0 +1,2 @@
|
||||
import 'expo-router/entry';
|
||||
import './unistyles';
|
||||
20
apps/cli/templates/frontend/native/unistyles/metro.config.js
Normal 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;
|
||||
47
apps/cli/templates/frontend/native/unistyles/package.json
Normal 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
|
||||
}
|
||||
35
apps/cli/templates/frontend/native/unistyles/theme.ts
Normal 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;
|
||||
12
apps/cli/templates/frontend/native/unistyles/tsconfig.json
Normal 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"]
|
||||
}
|
||||
27
apps/cli/templates/frontend/native/unistyles/unistyles.ts
Normal 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,
|
||||
},
|
||||
});
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||