add ai and todo example templates for native frontends (#293)

This commit is contained in:
Aman Varshney
2025-06-02 16:30:53 +05:30
committed by GitHub
parent 9dbeea8983
commit 7851d0636d
42 changed files with 1606 additions and 536 deletions

View File

@@ -1,27 +1,44 @@
import { TabBarIcon } from "@/components/tabbar-icon";
import { useColorScheme } from "@/lib/use-color-scheme";
import { Tabs } from "expo-router";
import { TabBarIcon } from "@/components/tabbar-icon";
export default function TabLayout() {
const { isDarkColorScheme } = useColorScheme();
return (
<Tabs
screenOptions={{
headerShown: false,
tabBarActiveTintColor: "black",
tabBarActiveTintColor: isDarkColorScheme
? "hsl(217.2 91.2% 59.8%)"
: "hsl(221.2 83.2% 53.3%)",
tabBarInactiveTintColor: isDarkColorScheme
? "hsl(215 20.2% 65.1%)"
: "hsl(215.4 16.3% 46.9%)",
tabBarStyle: {
backgroundColor: isDarkColorScheme
? "hsl(222.2 84% 4.9%)"
: "hsl(0 0% 100%)",
borderTopColor: isDarkColorScheme
? "hsl(217.2 32.6% 17.5%)"
: "hsl(214.3 31.8% 91.4%)",
},
}}
>
<Tabs.Screen
name="index"
options={{
title: "Tab One",
tabBarIcon: ({ color }) => <TabBarIcon name="code" color={color} />,
title: "Home",
tabBarIcon: ({ color }) => <TabBarIcon name="home" color={color} />,
}}
/>
<Tabs.Screen
name="two"
options={{
title: "Tab Two",
tabBarIcon: ({ color }) => <TabBarIcon name="code" color={color} />,
title: "Explore",
tabBarIcon: ({ color }) => (
<TabBarIcon name="compass" color={color} />
),
}}
/>
</Tabs>

View File

@@ -1,17 +1,19 @@
import { Container } from "@/components/container";
import { Text, View } from "react-native";
import { ScrollView, Text, View } from "react-native";
export default function TabOne() {
return (
<Container>
<View className="p-6 flex-1 justify-center">
<Text className="text-2xl font-bold text-foreground text-center mb-4">
Tab One
</Text>
<Text className="text-foreground text-center">
This is the first tab of the application.
</Text>
</View>
</Container>
);
return (
<Container>
<ScrollView className="flex-1 p-6">
<View className="py-8">
<Text className="text-3xl font-bold text-foreground mb-2">
Tab One
</Text>
<Text className="text-lg text-muted-foreground">
Explore the first section of your app
</Text>
</View>
</ScrollView>
</Container>
);
}

View File

@@ -1,17 +1,19 @@
import { Container } from "@/components/container";
import { Text, View } from "react-native";
import { ScrollView, Text, View } from "react-native";
export default function TabTwo() {
return (
<Container>
<View className="p-6 flex-1 justify-center">
<Text className="text-2xl font-bold text-foreground text-center mb-4">
Tab Two
</Text>
<Text className="text-foreground text-center">
This is the second tab of the application.
</Text>
</View>
</Container>
);
return (
<Container>
<ScrollView className="flex-1 p-6">
<View className="py-8">
<Text className="text-3xl font-bold text-foreground mb-2">
Tab Two
</Text>
<Text className="text-lg text-muted-foreground">
Discover more features and content
</Text>
</View>
</ScrollView>
</Container>
);
}

View File

@@ -17,18 +17,6 @@ const DrawerLayout = () => {
),
}}
/>
{{#if (includes examples "todo")}}
<Drawer.Screen
name="todos"
options=\{{
headerTitle: "Todos",
drawerLabel: "Todos",
drawerIcon: ({ size, color }) => (
<Ionicons name="checkbox-outline" size={size} color={color} />
),
}}
/>
{{/if}}
<Drawer.Screen
name="(tabs)"
options=\{{
@@ -44,6 +32,34 @@ const DrawerLayout = () => {
),
}}
/>
{{#if (includes examples "todo")}}
<Drawer.Screen
name="todos"
options=\{{
headerTitle: "Todos",
drawerLabel: "Todos",
drawerIcon: ({ size, color }) => (
<Ionicons name="checkbox-outline" size={size} color={color} />
),
}}
/>
{{/if}}
{{#if (includes examples "ai")}}
<Drawer.Screen
name="ai"
options=\{{
headerTitle: "AI",
drawerLabel: "AI",
drawerIcon: ({ size, color }) => (
<Ionicons
name="chatbubble-ellipses-outline"
size={size}
color={color}
/>
),
}}
/>
{{/if}}
</Drawer>
);
};

View File

@@ -26,52 +26,65 @@ export default function Home() {
return (
<Container>
<ScrollView className="py-4 flex-1">
<Text className="font-mono text-foreground text-2xl font-bold mb-6">
BETTER T STACK
</Text>
<View className="rounded-lg border border-foreground p-4">
<Text className="mb-2 font-medium text-foreground">API Status</Text>
<ScrollView showsVerticalScrollIndicator={false} className="flex-1">
<Text className="font-mono text-foreground text-3xl font-bold mb-4">
BETTER T STACK
</Text>
<View className="bg-card border border-border rounded-xl p-6 mb-6 shadow-sm">
{{#if (eq backend "convex")}}
<View className="flex-row items-center gap-2">
<View className="flex-row items-center gap-3">
<View
className={`h-2.5 w-2.5 rounded-full ${
healthCheck ? "bg-green-500" : "bg-red-500"
className={`h-3 w-3 rounded-full ${
healthCheck ? "bg-green-500" : "bg-orange-500"
}`}
/>
<Text className="text-sm text-foreground">
<View className="flex-1">
<Text className="text-sm font-medium text-card-foreground">
Convex
</Text>
<Text className="text-xs text-muted-foreground">
{healthCheck === undefined
? "Checking..."
? "Checking connection..."
: healthCheck === "OK"
? "Connected"
: "Error"}
</Text>
? "All systems operational"
: "Service unavailable"}
</Text>
</View>
</View>
{{else}}
{{#unless (eq api "none")}}
<View className="flex-row items-center gap-2">
<View className="flex-row items-center gap-3">
<View
className={`h-2.5 w-2.5 rounded-full ${
healthCheck.data ? "bg-green-500" : "bg-red-500"
className={`h-3 w-3 rounded-full ${
healthCheck.data ? "bg-green-500" : "bg-orange-500"
}`}
/>
<Text className="text-sm text-foreground">
{{#if (eq api "orpc")}}
<View className="flex-1">
<Text className="text-sm font-medium text-card-foreground">
{{#if (eq api "orpc")}}
ORPC
{{/if}}
{{#if (eq api "trpc")}}
TRPC
{{/if}}
</Text>
<Text className="text-xs text-muted-foreground">
{{#if (eq api "orpc")}}
{healthCheck.isLoading
? "Checking..."
? "Checking connection..."
: healthCheck.data
? "Connected"
: "Disconnected"}
{{/if}}
{{#if (eq api "trpc")}}
? "All systems operational"
: "Service unavailable"}
{{/if}}
{{#if (eq api "trpc")}}
{healthCheck.isLoading
? "Checking..."
? "Checking connection..."
: healthCheck.data
? "Connected"
: "Disconnected"}
{{/if}}
</Text>
? "All systems operational"
: "Service unavailable"}
{{/if}}
</Text>
</View>
</View>
{{/unless}}
{{/if}}

View File

@@ -1,17 +1,28 @@
import { Link, Stack } from 'expo-router';
import { Text } from 'react-native';
import { Container } from '@/components/container';
import { Container } from "@/components/container";
import { Link, Stack } from "expo-router";
import { Text, View } from "react-native";
export default function NotFoundScreen() {
return (
<>
<Stack.Screen options={{ title: 'Oops!' }} />
<Stack.Screen options={{ title: "Oops!" }} />
<Container>
<Text className="text-xl font-bold">This screen doesn't exist.</Text>
<Link href="/" className="mt-4 pt-4">
<Text className="text-base text-[#2e78b7]">Go to home screen!</Text>
</Link>
<View className="flex-1 justify-center items-center p-6">
<View className="items-center">
<Text className="text-6xl mb-4">🤔</Text>
<Text className="text-2xl font-bold text-foreground mb-2 text-center">
Page Not Found
</Text>
<Text className="text-muted-foreground text-center mb-8 max-w-sm">
Sorry, the page you're looking for doesn't exist.
</Text>
<Link href="/" asChild>
<Text className="text-primary font-medium bg-primary/10 px-6 py-3 rounded-lg">
Go to Home
</Text>
</Link>
</View>
</View>
</Container>
</>
);

View File

@@ -1,3 +1,6 @@
{{#if (includes examples "ai")}}
import "@/polyfills";
{{/if}}
{{#if (eq backend "convex")}}
import { ConvexProvider, ConvexReactClient } from "convex/react";
{{else}}

View File

@@ -2,11 +2,13 @@ import { Container } from "@/components/container";
import { Text, View } from "react-native";
export default function Modal() {
return (
<Container>
<View className="flex-1 justify-center items-center">
<Text className="text-xl font-bold text-foreground">Modal View</Text>
</View>
</Container>
);
return (
<Container>
<View className="flex-1 p-6">
<View className="flex-row items-center justify-between mb-8">
<Text className="text-2xl font-bold text-foreground">Modal</Text>
</View>
</View>
</Container>
);
}

View File

@@ -1,9 +1,8 @@
import React from "react";
import { SafeAreaView } from "react-native";
export const Container = ({ children }: { children: React.ReactNode }) => {
return (
<SafeAreaView className="flex flex-1 p-4 bg-background">
{children}
</SafeAreaView>
<SafeAreaView className="flex-1 bg-background">{children}</SafeAreaView>
);
};

View File

@@ -1,31 +1,26 @@
import FontAwesome from '@expo/vector-icons/FontAwesome';
import { forwardRef } from 'react';
import { Pressable, StyleSheet } from 'react-native';
import FontAwesome from "@expo/vector-icons/FontAwesome";
import { forwardRef } from "react";
import { Pressable } 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,
},
export const HeaderButton = forwardRef<
typeof Pressable,
{ onPress?: () => void }
>(({ onPress }, ref) => {
return (
<Pressable
onPress={onPress}
className="p-2 mr-2 rounded-lg bg-secondary/50 active:bg-secondary"
>
{({ pressed }) => (
<FontAwesome
name="info-circle"
size={20}
className="text-secondary-foreground"
style={{
opacity: pressed ? 0.7 : 1,
}}
/>
)}
</Pressable>
);
});

View File

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

View File

@@ -5,21 +5,46 @@
@layer base {
:root {
--background: 0 0% 100%;
--foreground: 240 10% 3.9%;
--primary: 240 5.9% 10%;
--primary-foreground: 0 0% 98%;
--secondary: 240 4.8% 95.9%;
--secondary-foreground: 240 5.9% 10%;
--foreground: 222.2 84% 4.9%;
--card: 0 0% 100%;
--card-foreground: 222.2 84% 4.9%;
--popover: 0 0% 100%;
--popover-foreground: 222.2 84% 4.9%;
--primary: 221.2 83.2% 53.3%;
--primary-foreground: 210 40% 98%;
--secondary: 210 40% 96%;
--secondary-foreground: 222.2 84% 4.9%;
--muted: 210 40% 96%;
--muted-foreground: 215.4 16.3% 40%;
--accent: 210 40% 96%;
--accent-foreground: 222.2 84% 4.9%;
--destructive: 0 84.2% 60.2%;
--destructive-foreground: 210 40% 98%;
--border: 214.3 31.8% 91.4%;
--input: 214.3 31.8% 91.4%;
--ring: 221.2 83.2% 53.3%;
--radius: 8px;
}
.dark:root {
--background: 240 10% 3.9%;
--foreground: 0 0% 98%;
--primary: 0 0% 98%;
--primary-foreground: 240 5.9% 10%;
--secondary: 240 3.7% 15.9%;
--secondary-foreground: 0 0% 98%;
--background: 222.2 84% 4.9%;
--foreground: 210 40% 98%;
--card: 222.2 84% 4.9%;
--card-foreground: 210 40% 98%;
--popover: 222.2 84% 4.9%;
--popover-foreground: 210 40% 98%;
--primary: 217.2 91.2% 59.8%;
--primary-foreground: 222.2 84% 4.9%;
--secondary: 217.2 32.6% 17.5%;
--secondary-foreground: 210 40% 98%;
--muted: 217.2 32.6% 17.5%;
--muted-foreground: 215 20.2% 70%;
--accent: 217.2 32.6% 17.5%;
--accent-foreground: 210 40% 98%;
--destructive: 0 72% 51%;
--destructive-foreground: 210 40% 98%;
--border: 217.2 32.6% 17.5%;
--input: 217.2 32.6% 17.5%;
--ring: 224.3 76.3% 94.1%;
}
}

View File

@@ -1,18 +1,18 @@
export const NAV_THEME = {
light: {
background: "hsl(0 0% 100%)",
border: "hsl(240 5.9% 90%)",
border: "hsl(220 13% 91%)",
card: "hsl(0 0% 100%)",
notification: "hsl(0 84.2% 60.2%)",
primary: "hsl(240 5.9% 10%)",
text: "hsl(240 10% 3.9%)",
primary: "hsl(221.2 83.2% 53.3%)",
text: "hsl(222.2 84% 4.9%)",
},
dark: {
background: "hsl(240 10% 3.9%)",
border: "hsl(240 3.7% 15.9%)",
card: "hsl(240 10% 3.9%)",
background: "hsl(222.2 84% 4.9%)",
border: "hsl(217.2 32.6% 17.5%)",
card: "hsl(222.2 84% 4.9%)",
notification: "hsl(0 72% 51%)",
primary: "hsl(0 0% 98%)",
text: "hsl(0 0% 98%)",
primary: "hsl(217.2 91.2% 59.8%)",
text: "hsl(210 40% 98%)",
},
};

View File

@@ -16,6 +16,10 @@
"@react-navigation/native": "^7.0.14",
"@tanstack/react-form": "^1.0.5",
"@tanstack/react-query": "^5.69.2",
{{#if (includes examples "ai")}}
"@stardazed/streams-text-encoding": "^1.0.2",
"@ungap/structured-clone": "^1.3.0",
{{/if}}
"expo": "^53.0.4",
"expo-constants": "~17.1.4",
"expo-linking": "~7.1.4",

View File

@@ -11,6 +11,14 @@ module.exports = {
colors: {
background: "hsl(var(--background))",
foreground: "hsl(var(--foreground))",
card: {
DEFAULT: "hsl(var(--card))",
foreground: "hsl(var(--card-foreground))",
},
popover: {
DEFAULT: "hsl(var(--popover))",
foreground: "hsl(var(--popover-foreground))",
},
primary: {
DEFAULT: "hsl(var(--primary))",
foreground: "hsl(var(--primary-foreground))",
@@ -19,9 +27,28 @@ module.exports = {
DEFAULT: "hsl(var(--secondary))",
foreground: "hsl(var(--secondary-foreground))",
},
muted: {
DEFAULT: "hsl(var(--muted))",
foreground: "hsl(var(--muted-foreground))",
},
accent: {
DEFAULT: "hsl(var(--accent))",
foreground: "hsl(var(--accent-foreground))",
},
destructive: {
DEFAULT: "hsl(var(--destructive))",
foreground: "hsl(var(--destructive-foreground))",
},
border: "hsl(var(--border))",
input: "hsl(var(--input))",
ring: "hsl(var(--ring))",
radius: "var(--radius)",
},
borderRadius: {
xl: "calc(var(--radius) + 4px)",
lg: "var(--radius)",
md: "calc(var(--radius) - 2px)",
sm: "calc(var(--radius) - 4px)",
},
borderWidth: {
hairline: hairlineWidth(),