Add Native Todo Example Support (#285)

This commit is contained in:
Igor Belogurov
2025-05-27 06:52:01 +02:00
committed by Aman Varshney
parent 41ec89a79a
commit 906b55599b
3 changed files with 312 additions and 0 deletions

View File

@@ -0,0 +1,5 @@
---
"create-better-t-stack": patch
---
Add Native Todo Example Support

View File

@@ -0,0 +1,295 @@
import { useState } from "react";
import {
View,
Text,
TextInput,
TouchableOpacity,
ScrollView,
ActivityIndicator,
Alert,
} from "react-native";
import { Ionicons } from "@expo/vector-icons";
{{#if (eq backend "convex")}}
import { useMutation, useQuery } from "convex/react";
import { api } from "@{{projectName}}/backend/convex/_generated/api.js";
import type { Id } from "@{{projectName}}/backend/convex/_generated/dataModel.d.ts";
{{else}}
import { useMutation, useQuery } from "@tanstack/react-query";
{{/if}}
import { Container } from "@/components/container";
{{#unless (eq backend "convex")}}
{{#if (eq api "orpc")}}
import { orpc } from "@/utils/orpc";
{{/if}}
{{#if (eq api "trpc")}}
import { trpc } from "@/utils/trpc";
{{/if}}
{{/unless}}
export default function TodosScreen() {
const [newTodoText, setNewTodoText] = useState("");
{{#if (eq backend "convex")}}
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 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 }),
},
]);
};
{{else}}
{{#if (eq api "orpc")}}
const todos = useQuery(orpc.todo.getAll.queryOptions());
const createMutation = useMutation(
orpc.todo.create.mutationOptions({
onSuccess: () => {
todos.refetch();
setNewTodoText("");
},
}),
);
const toggleMutation = useMutation(
orpc.todo.toggle.mutationOptions({
onSuccess: () => todos.refetch(),
}),
);
const deleteMutation = useMutation(
orpc.todo.delete.mutationOptions({
onSuccess: () => todos.refetch(),
}),
);
{{/if}}
{{#if (eq api "trpc")}}
const todos = useQuery(trpc.todo.getAll.queryOptions());
const createMutation = useMutation(
trpc.todo.create.mutationOptions({
onSuccess: () => {
todos.refetch();
setNewTodoText("");
},
}),
);
const toggleMutation = useMutation(
trpc.todo.toggle.mutationOptions({
onSuccess: () => todos.refetch(),
}),
);
const deleteMutation = useMutation(
trpc.todo.delete.mutationOptions({
onSuccess: () => todos.refetch(),
}),
);
{{/if}}
const handleAddTodo = () => {
if (newTodoText.trim()) {
createMutation.mutate({ text: newTodoText });
}
};
const handleToggleTodo = (id: number, completed: boolean) => {
toggleMutation.mutate({ id, completed: !completed });
};
const handleDeleteTodo = (id: number) => {
Alert.alert("Delete Todo", "Are you sure you want to delete this todo?", [
{ text: "Cancel", style: "cancel" },
{
text: "Delete",
style: "destructive",
onPress: () => deleteMutation.mutate({ id }),
},
]);
};
{{/if}}
return (
<Container>
<ScrollView className="flex-1">
<View className="px-4 py-6">
<View className="mb-6 rounded-lg border border-border p-4 bg-card">
<Text className="text-foreground text-2xl font-bold mb-2">
Todo List
</Text>
<Text className="text-muted-foreground mb-4">
Manage your tasks efficiently
</Text>
<View className="mb-6">
<View className="flex-row items-center space-x-2 mb-2">
<TextInput
value={newTodoText}
onChangeText={setNewTodoText}
placeholder="Add a new task..."
placeholderTextColor="#6b7280"
{{#if (eq backend "convex")}}
{{else}}
editable={!createMutation.isPending}
{{/if}}
className="flex-1 border border-border rounded-md px-3 py-2 text-foreground bg-background"
onSubmitEditing={handleAddTodo}
returnKeyType="done"
/>
<TouchableOpacity
onPress={handleAddTodo}
{{#if (eq backend "convex")}}
disabled={!newTodoText.trim()}
{{else}}
disabled={createMutation.isPending || !newTodoText.trim()}
{{/if}}
className={`px-4 py-2 rounded-md ${
{{#if (eq backend "convex")}}
!newTodoText.trim()
{{else}}
createMutation.isPending || !newTodoText.trim()
{{/if}}
? "bg-muted"
: "bg-primary"
}`}
>
{{#if (eq backend "convex")}}
<Text className="text-white font-medium">Add</Text>
{{else}}
{createMutation.isPending ? (
<ActivityIndicator size="small" color="white" />
) : (
<Text className="text-white font-medium">Add</Text>
)}
{{/if}}
</TouchableOpacity>
</View>
</View>
{{#if (eq backend "convex")}}
{todos === undefined ? (
<View className="flex justify-center py-8">
<ActivityIndicator size="large" color="#3b82f6" />
</View>
) : todos.length === 0 ? (
<Text className="py-8 text-center text-muted-foreground">
No todos yet. Add one above!
</Text>
) : (
<View className="space-y-2">
{todos.map((todo) => (
<View
key={todo._id}
className="flex-row items-center justify-between rounded-md border border-border p-3 bg-background"
>
<View className="flex-row items-center flex-1">
<TouchableOpacity
onPress={() =>
handleToggleTodo(todo._id, todo.completed)
}
className="mr-3"
>
<Ionicons
name={todo.completed ? "checkbox" : "square-outline"}
size={24}
color={todo.completed ? "#22c55e" : "#6b7280"}
/>
</TouchableOpacity>
<Text
className={`flex-1 ${
todo.completed
? "line-through text-muted-foreground"
: "text-foreground"
}`}
>
{todo.text}
</Text>
</View>
<TouchableOpacity
onPress={() => handleDeleteTodo(todo._id)}
className="ml-2 p-1"
>
<Ionicons
name="trash-outline"
size={20}
color="#ef4444"
/>
</TouchableOpacity>
</View>
))}
</View>
)}
{{else}}
{todos.isLoading ? (
<View className="flex justify-center py-8">
<ActivityIndicator size="large" color="#3b82f6" />
</View>
) : todos.data?.length === 0 ? (
<Text className="py-8 text-center text-muted-foreground">
No todos yet. Add one above!
</Text>
) : (
<View className="space-y-2">
{todos.data?.map((todo) => (
<View
key={todo.id}
className="flex-row items-center justify-between rounded-md border border-border p-3 bg-background"
>
<View className="flex-row items-center flex-1">
<TouchableOpacity
onPress={() =>
handleToggleTodo(todo.id, todo.completed)
}
className="mr-3"
>
<Ionicons
name={todo.completed ? "checkbox" : "square-outline"}
size={24}
color={todo.completed ? "#22c55e" : "#6b7280"}
/>
</TouchableOpacity>
<Text
className={`flex-1 ${
todo.completed
? "line-through text-muted-foreground"
: "text-foreground"
}`}
>
{todo.text}
</Text>
</View>
<TouchableOpacity
onPress={() => handleDeleteTodo(todo.id)}
className="ml-2 p-1"
>
<Ionicons
name="trash-outline"
size={20}
color="#ef4444"
/>
</TouchableOpacity>
</View>
))}
</View>
)}
{{/if}}
</View>
</View>
</ScrollView>
</Container>
);
}

View File

@@ -17,6 +17,18 @@ 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 <Drawer.Screen
name="(tabs)" name="(tabs)"
options={{ options={{