initial commit

This commit is contained in:
-Puter
2026-07-19 02:46:47 +05:30
commit 8033a8edb0
121 changed files with 7845 additions and 0 deletions

View File

@@ -0,0 +1,42 @@
import { Ionicons } from "@expo/vector-icons";
import { Tabs } from "expo-router";
import { useThemeColor } from "heroui-native";
export default function TabLayout() {
const themeColorForeground = useThemeColor("foreground");
const themeColorBackground = useThemeColor("background");
return (
<Tabs
screenOptions={{
headerShown: false,
headerStyle: {
backgroundColor: themeColorBackground,
},
headerTintColor: themeColorForeground,
headerTitleStyle: {
color: themeColorForeground,
fontWeight: "600",
},
tabBarStyle: {
backgroundColor: themeColorBackground,
},
}}
>
<Tabs.Screen
name="index"
options={{
title: "Home",
tabBarIcon: ({ color, size }) => <Ionicons name="home" size={size} color={color} />,
}}
/>
<Tabs.Screen
name="two"
options={{
title: "Explore",
tabBarIcon: ({ color, size }) => <Ionicons name="compass" size={size} color={color} />,
}}
/>
</Tabs>
);
}

View File

@@ -0,0 +1,16 @@
import { Card } from "heroui-native";
import { Text, View } from "react-native";
import { Container } from "@/components/container";
export default function Home() {
return (
<Container className="p-6">
<View className="flex-1 justify-center items-center">
<Card variant="secondary" className="p-8 items-center">
<Card.Title className="text-3xl mb-2">Tab One</Card.Title>
</Card>
</View>
</Container>
);
}

View File

@@ -0,0 +1,16 @@
import { Card } from "heroui-native";
import { Text, View } from "react-native";
import { Container } from "@/components/container";
export default function TabTwo() {
return (
<Container className="p-6">
<View className="flex-1 justify-center items-center">
<Card variant="secondary" className="p-8 items-center">
<Card.Title className="text-3xl mb-2">TabTwo</Card.Title>
</Card>
</View>
</Container>
);
}

View File

@@ -0,0 +1,88 @@
import { Ionicons, MaterialIcons } from "@expo/vector-icons";
import { Link } from "expo-router";
import { Drawer } from "expo-router/drawer";
import { useThemeColor } from "heroui-native";
import React, { useCallback } from "react";
import { Pressable, Text } from "react-native";
import { ThemeToggle } from "@/components/theme-toggle";
function DrawerLayout() {
const themeColorForeground = useThemeColor("foreground");
const themeColorBackground = useThemeColor("background");
const renderThemeToggle = useCallback(() => <ThemeToggle />, []);
return (
<Drawer
screenOptions={{
headerTintColor: themeColorForeground,
headerStyle: { backgroundColor: themeColorBackground },
headerTitleStyle: {
fontWeight: "600",
color: themeColorForeground,
},
headerRight: renderThemeToggle,
drawerStyle: { backgroundColor: themeColorBackground },
}}
>
<Drawer.Screen
name="index"
options={{
headerTitle: "Home",
drawerLabel: ({ color, focused }) => (
<Text style={{ color: focused ? color : themeColorForeground }}>Home</Text>
),
drawerIcon: ({ size, color, focused }) => (
<Ionicons
name="home-outline"
size={size}
color={focused ? color : themeColorForeground}
/>
),
}}
/>
<Drawer.Screen
name="(tabs)"
options={{
headerTitle: "Tabs",
drawerLabel: ({ color, focused }) => (
<Text style={{ color: focused ? color : themeColorForeground }}>Tabs</Text>
),
drawerIcon: ({ size, color, focused }) => (
<MaterialIcons
name="border-bottom"
size={size}
color={focused ? color : themeColorForeground}
/>
),
headerRight: () => (
<Link href="/modal" asChild>
<Pressable className="mr-4">
<Ionicons name="add-outline" size={24} color={themeColorForeground} />
</Pressable>
</Link>
),
}}
/>
<Drawer.Screen
name="todos"
options={{
headerTitle: "Todos",
drawerLabel: ({ color, focused }) => (
<Text style={{ color: focused ? color : themeColorForeground }}>Todos</Text>
),
drawerIcon: ({ size, color, focused }) => (
<Ionicons
name="checkbox-outline"
size={size}
color={focused ? color : themeColorForeground}
/>
),
}}
/>
</Drawer>
);
}
export default DrawerLayout;

View File

@@ -0,0 +1,72 @@
import { api } from "@code/backend/convex/_generated/api";
import { useConvexAuth, useQuery } from "convex/react";
import { Button, Chip, Separator, Spinner, Surface, useThemeColor } from "heroui-native";
import { Text, View } from "react-native";
import { Container } from "@/components/container";
import { SignIn } from "@/components/sign-in";
import { SignUp } from "@/components/sign-up";
import { authClient } from "@/lib/auth-client";
export default function Home() {
const healthCheck = useQuery(api.healthCheck.get);
const { isAuthenticated } = useConvexAuth();
const user = useQuery(api.auth.getCurrentUser, isAuthenticated ? {} : "skip");
const successColor = useThemeColor("success");
const dangerColor = useThemeColor("danger");
const isConnected = healthCheck === "OK";
const isLoading = healthCheck === undefined;
return (
<Container className="px-4 pb-4">
<View className="py-6 mb-5">
<Text className="text-3xl font-semibold text-foreground tracking-tight">
Better T Stack
</Text>
<Text className="text-muted text-sm mt-1">Full-stack TypeScript starter</Text>
</View>
{user ? (
<Surface variant="secondary" className="mb-4 p-4 rounded-xl">
<View className="flex-row items-center justify-between">
<View className="flex-1">
<Text className="text-foreground font-medium">{user.name}</Text>
<Text className="text-muted text-xs mt-0.5">{user.email}</Text>
</View>
<Button
variant="danger"
size="sm"
onPress={() => {
authClient.signOut();
}}
>
Sign Out
</Button>
</View>
</Surface>
) : null}
<Surface variant="secondary" className="p-4 rounded-xl">
<Text className="text-foreground font-medium mb-2">API Status</Text>
<View className="flex-row items-center gap-2">
<View
className={`w-2 h-2 rounded-full ${healthCheck === "OK" ? "bg-success" : "bg-danger"}`}
/>
<Text className="text-muted text-xs">
{healthCheck === undefined
? "Checking..."
: healthCheck === "OK"
? "Connected to API"
: "API Disconnected"}
</Text>
</View>
</Surface>
{!user && (
<View className="mt-5 gap-4">
<SignIn />
<SignUp />
</View>
)}
</Container>
);
}

View File

@@ -0,0 +1,149 @@
import { api } from "@code/backend/convex/_generated/api";
import type { Doc, Id } from "@code/backend/convex/_generated/dataModel";
import { Ionicons } from "@expo/vector-icons";
import { useMutation, useQuery } from "convex/react";
import {
Button,
Checkbox,
Chip,
Spinner,
Surface,
Input,
TextField,
useThemeColor,
} from "heroui-native";
import { useState } from "react";
import { View, Text, ScrollView, Alert } from "react-native";
import { Container } from "@/components/container";
export default function TodosScreen() {
const [newTodoText, setNewTodoText] = useState("");
const todos = useQuery(api.todos.getAll);
const createTodoMutation = useMutation(api.todos.create);
const toggleTodoMutation = useMutation(api.todos.toggle);
const deleteTodoMutation = useMutation(api.todos.deleteTodo);
const mutedColor = useThemeColor("muted");
const dangerColor = useThemeColor("danger");
const foregroundColor = useThemeColor("foreground");
const handleAddTodo = async () => {
const text = newTodoText.trim();
if (!text) return;
await createTodoMutation({ text });
setNewTodoText("");
};
const handleToggleTodo = (id: Id<"todos">, currentCompleted: boolean) => {
toggleTodoMutation({ id, completed: !currentCompleted });
};
const handleDeleteTodo = (id: Id<"todos">) => {
Alert.alert("Delete Todo", "Are you sure you want to delete this todo?", [
{ text: "Cancel", style: "cancel" },
{
text: "Delete",
style: "destructive",
onPress: () => deleteTodoMutation({ id }),
},
]);
};
const isLoading = !todos;
const completedCount = todos?.filter((t: Doc<"todos">) => t.completed).length || 0;
const totalCount = todos?.length || 0;
return (
<Container>
<ScrollView className="flex-1" contentContainerClassName="p-4">
<View className="py-4 mb-4">
<View className="flex-row items-center justify-between">
<Text className="text-2xl font-semibold text-foreground tracking-tight">Tasks</Text>
{totalCount > 0 && (
<Chip variant="secondary" color="accent" size="sm">
<Chip.Label>
{completedCount}/{totalCount}
</Chip.Label>
</Chip>
)}
</View>
</View>
<Surface variant="secondary" className="mb-4 p-3 rounded-lg">
<View className="flex-row items-center gap-2">
<View className="flex-1">
<TextField>
<Input
value={newTodoText}
onChangeText={setNewTodoText}
placeholder="Add a new task..."
onSubmitEditing={handleAddTodo}
returnKeyType="done"
/>
</TextField>
</View>
<Button
isIconOnly
variant={!newTodoText.trim() ? "secondary" : "primary"}
isDisabled={!newTodoText.trim()}
onPress={handleAddTodo}
size="sm"
>
<Ionicons
name="add"
size={20}
color={newTodoText.trim() ? foregroundColor : mutedColor}
/>
</Button>
</View>
</Surface>
{isLoading && (
<View className="items-center justify-center py-12">
<Spinner size="lg" />
<Text className="text-muted text-sm mt-3">Loading tasks...</Text>
</View>
)}
{todos && todos.length === 0 && !isLoading && (
<Surface variant="secondary" className="items-center justify-center py-10 rounded-lg">
<Ionicons name="checkbox-outline" size={40} color={mutedColor} />
<Text className="text-foreground font-medium mt-3">No tasks yet</Text>
<Text className="text-muted text-xs mt-1">Add your first task to get started</Text>
</Surface>
)}
{todos && todos.length > 0 && (
<View className="gap-2">
{todos.map((todo: Doc<"todos">) => (
<Surface key={todo._id} variant="secondary" className="p-3 rounded-lg">
<View className="flex-row items-center gap-3">
<Checkbox
isSelected={todo.completed}
onSelectedChange={() => handleToggleTodo(todo._id, todo.completed)}
/>
<View className="flex-1">
<Text
className={`text-sm ${todo.completed ? "text-muted line-through" : "text-foreground"}`}
>
{todo.text}
</Text>
</View>
<Button
isIconOnly
variant="ghost"
onPress={() => handleDeleteTodo(todo._id)}
size="sm"
>
<Ionicons name="trash-outline" size={16} color={dangerColor} />
</Button>
</View>
</Surface>
))}
</View>
)}
</ScrollView>
</Container>
);
}

View File

@@ -0,0 +1,27 @@
import { Link, Stack } from "expo-router";
import { Button, Surface } from "heroui-native";
import { Text, View } from "react-native";
import { Container } from "@/components/container";
export default function NotFoundScreen() {
return (
<>
<Stack.Screen options={{ title: "Not Found" }} />
<Container>
<View className="flex-1 justify-center items-center p-4">
<Surface variant="secondary" className="items-center p-6 max-w-sm rounded-lg">
<Text className="text-4xl mb-3">🤔</Text>
<Text className="text-foreground font-medium text-lg mb-1">Page Not Found</Text>
<Text className="text-muted text-sm text-center mb-4">
The page you're looking for doesn't exist.
</Text>
<Link href="/" asChild>
<Button size="sm">Go Home</Button>
</Link>
</Surface>
</View>
</Container>
</>
);
}

View File

@@ -0,0 +1,44 @@
import "@/global.css";
import { env } from "@code/env/native";
import { ConvexBetterAuthProvider } from "@convex-dev/better-auth/react";
import { ConvexReactClient } from "convex/react";
import { Stack } from "expo-router";
import { HeroUINativeProvider } from "heroui-native";
import { GestureHandlerRootView } from "react-native-gesture-handler";
import { KeyboardProvider } from "react-native-keyboard-controller";
import { AppThemeProvider } from "@/contexts/app-theme-context";
import { authClient } from "@/lib/auth-client";
export const unstable_settings = {
initialRouteName: "(drawer)",
};
const convex = new ConvexReactClient(env.EXPO_PUBLIC_CONVEX_URL, {
unsavedChangesWarning: false,
});
function StackLayout() {
return (
<Stack screenOptions={{}}>
<Stack.Screen name="(drawer)" options={{ headerShown: false }} />
<Stack.Screen name="modal" options={{ title: "Modal", presentation: "modal" }} />
</Stack>
);
}
export default function Layout() {
return (
<ConvexBetterAuthProvider client={convex} authClient={authClient}>
<GestureHandlerRootView style={{ flex: 1 }}>
<KeyboardProvider>
<AppThemeProvider>
<HeroUINativeProvider>
<StackLayout />
</HeroUINativeProvider>
</AppThemeProvider>
</KeyboardProvider>
</GestureHandlerRootView>
</ConvexBetterAuthProvider>
);
}

37
apps/native/app/modal.tsx Normal file
View File

@@ -0,0 +1,37 @@
import { Ionicons } from "@expo/vector-icons";
import { router } from "expo-router";
import { Button, Surface, useThemeColor } from "heroui-native";
import { Text, View } from "react-native";
import { Container } from "@/components/container";
function Modal() {
const accentForegroundColor = useThemeColor("accent-foreground");
function handleClose() {
router.back();
}
return (
<Container>
<View className="flex-1 justify-center items-center p-4">
<Surface variant="secondary" className="p-5 w-full max-w-sm rounded-lg">
<View className="items-center">
<View className="w-12 h-12 bg-accent rounded-lg items-center justify-center mb-3">
<Ionicons name="checkmark" size={24} color={accentForegroundColor} />
</View>
<Text className="text-foreground font-medium text-lg mb-1">Modal Screen</Text>
<Text className="text-muted text-sm text-center mb-4">
This is an example modal screen for dialogs and confirmations.
</Text>
</View>
<Button onPress={handleClose} className="w-full" size="sm">
<Button.Label>Close</Button.Label>
</Button>
</Surface>
</View>
</Container>
);
}
export default Modal;