UNPKG

@reliverse/rse

Version:

@reliverse/rse is your all-in-one companion for bootstrapping and improving any kind of projects (especially web apps built with frameworks like Next.js) — whether you're kicking off something new or upgrading an existing app. It is also a little AI-power

1,542 lines 111 kB
export const DLER_TPL_EXAMPLES = { name: "examples", description: "Template generated from 31 files", updatedAt: "2025-06-17T20:33:59.686Z", config: { files: { "examples/ai/native/nativewind/app/(drawer)/ai.tsx.hbs": { metadata: { updatedAt: "2025-06-17T06:06:35.000Z", updatedHash: "cc30ba62a9" }, content: `import { useRef, useEffect } from "react"; import { View, Text, TextInput, TouchableOpacity, ScrollView, KeyboardAvoidingView, Platform, } from "react-native"; import { useChat } from "@ai-sdk/react"; import { fetch as expoFetch } from "expo/fetch"; import { Ionicons } from "@expo/vector-icons"; import { Container } from "@/components/container"; // Utility function to generate API URLs const generateAPIUrl = (relativePath: string) => { const serverUrl = process.env.EXPO_PUBLIC_SERVER_URL; if (!serverUrl) { throw new Error("EXPO_PUBLIC_SERVER_URL environment variable is not defined"); } const path = relativePath.startsWith('/') ? relativePath : \`/\${relativePath}\`; return serverUrl.concat(path); }; export default function AIScreen() { const { messages, input, handleInputChange, handleSubmit, error } = useChat({ fetch: expoFetch as unknown as typeof globalThis.fetch, api: generateAPIUrl('/ai'), onError: error => console.error(error, 'AI Chat Error'), maxSteps: 5, }); const scrollViewRef = useRef<ScrollView>(null); useEffect(() => { scrollViewRef.current?.scrollToEnd({ animated: true }); }, [messages]); const onSubmit = () => { if (input.trim()) { handleSubmit(); } }; if (error) { return ( <Container> <View className="flex-1 justify-center items-center px-4"> <Text className="text-destructive text-center text-lg mb-4"> Error: {error.message} </Text> <Text className="text-muted-foreground text-center"> Please check your connection and try again. </Text> </View> </Container> ); } return ( <Container> <KeyboardAvoidingView className="flex-1" behavior={Platform.OS === "ios" ? "padding" : "height"} > <View className="flex-1 px-4 py-6"> <View className="mb-6"> <Text className="text-foreground text-2xl font-bold mb-2"> AI Chat </Text> <Text className="text-muted-foreground"> Chat with our AI assistant </Text> </View> <ScrollView ref={scrollViewRef} className="flex-1 mb-4" showsVerticalScrollIndicator={false} > {messages.length === 0 ? ( <View className="flex-1 justify-center items-center"> <Text className="text-center text-muted-foreground text-lg"> Ask me anything to get started! </Text> </View> ) : ( <View className="space-y-4"> {messages.map((message) => ( <View key={message.id} className={\`p-3 rounded-lg \${ message.role === "user" ? "bg-primary/10 ml-8" : "bg-card mr-8 border border-border" }\`} > <Text className="text-sm font-semibold mb-1 text-foreground"> {message.role === "user" ? "You" : "AI Assistant"} </Text> <Text className="text-foreground leading-relaxed"> {message.content} </Text> </View> ))} </View> )} </ScrollView> <View className="border-t border-border pt-4"> <View className="flex-row items-end space-x-2"> <TextInput value={input} onChange={(e) => handleInputChange({ ...e, target: { ...e.target, value: e.nativeEvent.text, }, } as unknown as React.ChangeEvent<HTMLInputElement>) } placeholder="Type your message..." placeholderTextColor="#6b7280" className="flex-1 border border-border rounded-md px-3 py-2 text-foreground bg-background min-h-[40px] max-h-[120px]" onSubmitEditing={(e) => { handleSubmit(e); e.preventDefault(); }} autoFocus={true} /> <TouchableOpacity onPress={onSubmit} disabled={!input.trim()} className={\`p-2 rounded-md \${ input.trim() ? "bg-primary" : "bg-muted" }\`} > <Ionicons name="send" size={20} color={input.trim() ? "#ffffff" : "#6b7280"} /> </TouchableOpacity> </View> </View> </View> </KeyboardAvoidingView> </Container> ); } `, type: "text" }, "examples/ai/native/nativewind/polyfills.js": { metadata: { updatedAt: "2025-06-17T06:06:35.000Z", updatedHash: "9866d77e7e" }, content: `import structuredClone from "@ungap/structured-clone"; import { Platform } from "react-native"; if (Platform.OS !== "web") { const setupPolyfills = async () => { const { polyfillGlobal } = await import( "react-native/Libraries/Utilities/PolyfillFunctions" ); const { TextEncoderStream, TextDecoderStream } = await import( "@stardazed/streams-text-encoding" ); if (!("structuredClone" in global)) { polyfillGlobal("structuredClone", () => structuredClone); } polyfillGlobal("TextEncoderStream", () => TextEncoderStream); polyfillGlobal("TextDecoderStream", () => TextDecoderStream); }; setupPolyfills(); } export {}; `, type: "text" }, "examples/ai/native/unistyles/app/(drawer)/ai.tsx.hbs": { metadata: { updatedAt: "2025-06-17T06:06:35.000Z", updatedHash: "3eab7496a4" }, content: `import { useRef, useEffect } from "react"; import { View, Text, TextInput, TouchableOpacity, ScrollView, KeyboardAvoidingView, Platform, } from "react-native"; import { useChat } from "@ai-sdk/react"; import { fetch as expoFetch } from "expo/fetch"; import { Ionicons } from "@expo/vector-icons"; import { StyleSheet, useUnistyles } from "react-native-unistyles"; import { Container } from "@/components/container"; const generateAPIUrl = (relativePath: string) => { const serverUrl = process.env.EXPO_PUBLIC_SERVER_URL; if (!serverUrl) { throw new Error( "EXPO_PUBLIC_SERVER_URL environment variable is not defined", ); } const path = relativePath.startsWith("/") ? relativePath : \`/\${relativePath}\`; return serverUrl.concat(path); }; export default function AIScreen() { const { theme } = useUnistyles(); const { messages, input, handleInputChange, handleSubmit, error } = useChat({ fetch: expoFetch as unknown as typeof globalThis.fetch, api: generateAPIUrl("/ai"), onError: (error) => console.error(error, "AI Chat Error"), maxSteps: 5, }); const scrollViewRef = useRef<ScrollView>(null); useEffect(() => { scrollViewRef.current?.scrollToEnd({ animated: true }); }, [messages]); const onSubmit = () => { if (input.trim()) { handleSubmit(); } }; if (error) { return ( <Container> <View style={styles.errorContainer}> <Text style={styles.errorText}>Error: {error.message}</Text> <Text style={styles.errorSubtext}> Please check your connection and try again. </Text> </View> </Container> ); } return ( <Container> <KeyboardAvoidingView style={styles.container} behavior={Platform.OS === "ios" ? "padding" : "height"} > <View style={styles.content}> <View style={styles.header}> <Text style={styles.headerTitle}>AI Chat</Text> <Text style={styles.headerSubtitle}> Chat with our AI assistant </Text> </View> <ScrollView ref={scrollViewRef} style={styles.messagesContainer} showsVerticalScrollIndicator={false} > {messages.length === 0 ? ( <View style={styles.emptyContainer}> <Text style={styles.emptyText}> Ask me anything to get started! </Text> </View> ) : ( <View style={styles.messagesWrapper}> {messages.map((message) => ( <View key={message.id} style={[ styles.messageContainer, message.role === "user" ? styles.userMessage : styles.assistantMessage, ]} > <Text style={styles.messageRole}> {message.role === "user" ? "You" : "AI Assistant"} </Text> <Text style={styles.messageContent}>{message.content}</Text> </View> ))} </View> )} </ScrollView> <View style={styles.inputSection}> <View style={styles.inputContainer}> <TextInput value={input} onChange={(e) => handleInputChange({ ...e, target: { ...e.target, value: e.nativeEvent.text, }, } as unknown as React.ChangeEvent<HTMLInputElement>) } placeholder="Type your message..." placeholderTextColor={theme.colors.border} style={styles.textInput} onSubmitEditing={(e) => { handleSubmit(e); e.preventDefault(); }} autoFocus={true} /> <TouchableOpacity onPress={onSubmit} disabled={!input.trim()} style={[ styles.sendButton, !input.trim() && styles.sendButtonDisabled, ]} > <Ionicons name="send" size={20} color={ input.trim() ? theme.colors.background : theme.colors.border } /> </TouchableOpacity> </View> </View> </View> </KeyboardAvoidingView> </Container> ); } const styles = StyleSheet.create((theme) => ({ container: { flex: 1, }, content: { flex: 1, paddingHorizontal: theme.spacing.md, paddingVertical: theme.spacing.lg, }, errorContainer: { flex: 1, justifyContent: "center", alignItems: "center", paddingHorizontal: theme.spacing.md, }, errorText: { color: theme.colors.destructive, textAlign: "center", fontSize: 18, marginBottom: theme.spacing.md, }, errorSubtext: { color: theme.colors.typography, textAlign: "center", fontSize: 16, }, header: { marginBottom: theme.spacing.lg, }, headerTitle: { fontSize: 28, fontWeight: "bold", color: theme.colors.typography, marginBottom: theme.spacing.sm, }, headerSubtitle: { fontSize: 16, color: theme.colors.typography, }, messagesContainer: { flex: 1, marginBottom: theme.spacing.md, }, emptyContainer: { flex: 1, justifyContent: "center", alignItems: "center", }, emptyText: { textAlign: "center", color: theme.colors.typography, fontSize: 18, }, messagesWrapper: { gap: theme.spacing.md, }, messageContainer: { padding: theme.spacing.md, borderRadius: 8, }, userMessage: { backgroundColor: theme.colors.primary + "20", marginLeft: theme.spacing.xl, alignSelf: "flex-end", }, assistantMessage: { backgroundColor: theme.colors.background, marginRight: theme.spacing.xl, borderWidth: 1, borderColor: theme.colors.border, }, messageRole: { fontSize: 14, fontWeight: "600", marginBottom: theme.spacing.sm, color: theme.colors.typography, }, messageContent: { color: theme.colors.typography, lineHeight: 20, }, toolInvocations: { fontSize: 12, color: theme.colors.typography, fontFamily: "monospace", backgroundColor: theme.colors.border + "40", padding: theme.spacing.sm, borderRadius: 4, marginTop: theme.spacing.sm, }, inputSection: { borderTopWidth: 1, borderTopColor: theme.colors.border, paddingTop: theme.spacing.md, }, inputContainer: { flexDirection: "row", alignItems: "flex-end", gap: theme.spacing.sm, }, textInput: { flex: 1, borderWidth: 1, borderColor: theme.colors.border, borderRadius: 8, paddingHorizontal: theme.spacing.md, paddingVertical: theme.spacing.sm, color: theme.colors.typography, backgroundColor: theme.colors.background, fontSize: 16, minHeight: 40, maxHeight: 120, }, sendButton: { backgroundColor: theme.colors.primary, padding: theme.spacing.sm, borderRadius: 8, justifyContent: "center", alignItems: "center", }, sendButtonDisabled: { backgroundColor: theme.colors.border, }, })); `, type: "text" }, "examples/ai/native/unistyles/polyfills.js": { metadata: { updatedAt: "2025-06-17T06:06:35.000Z", updatedHash: "9866d77e7e" }, content: `import structuredClone from "@ungap/structured-clone"; import { Platform } from "react-native"; if (Platform.OS !== "web") { const setupPolyfills = async () => { const { polyfillGlobal } = await import( "react-native/Libraries/Utilities/PolyfillFunctions" ); const { TextEncoderStream, TextDecoderStream } = await import( "@stardazed/streams-text-encoding" ); if (!("structuredClone" in global)) { polyfillGlobal("structuredClone", () => structuredClone); } polyfillGlobal("TextEncoderStream", () => TextEncoderStream); polyfillGlobal("TextDecoderStream", () => TextDecoderStream); }; setupPolyfills(); } export {}; `, type: "text" }, "examples/ai/server/next/src/app/ai/route.ts": { metadata: { updatedAt: "2025-06-17T06:06:35.000Z", updatedHash: "eee0d6a73d" }, content: `import { google } from '@ai-sdk/google'; import { streamText } from 'ai'; export const maxDuration = 30; export async function POST(req: Request) { const { messages } = await req.json(); const result = streamText({ model: google('gemini-2.0-flash'), messages, }); return result.toDataStreamResponse(); } `, type: "text" }, "examples/ai/web/nuxt/app/pages/ai.vue": { metadata: { updatedAt: "2025-06-17T06:06:35.000Z", updatedHash: "7dd81ef739" }, content: `<script setup lang="ts"> import { useChat } from '@ai-sdk/vue' import { nextTick, ref, watch } from 'vue' const config = useRuntimeConfig() const serverUrl = config.public.serverURL const { messages, input, handleSubmit } = useChat({ api: \`\${serverUrl}/ai\`, }) const messagesEndRef = ref<null | HTMLDivElement>(null) watch(messages, async () => { await nextTick() messagesEndRef.value?.scrollIntoView({ behavior: 'smooth' }) }) function getMessageText(message: any) { return message.parts .filter((part: any) => part.type === 'text') .map((part: any) => part.text) .join('') } <\/script> <template> <div class="grid grid-rows-[1fr_auto] overflow-hidden w-full mx-auto p-4"> <div class="overflow-y-auto space-y-4 pb-4"> <div v-if="messages.length === 0" class="text-center text-muted-foreground mt-8"> Ask me anything to get started! </div> <div v-for="message in messages" :key="message.id" :class="[ 'p-3 rounded-lg', message.role === 'user' ? 'bg-primary/10 ml-8' : 'bg-secondary/20 mr-8' ]" > <p class="text-sm font-semibold mb-1"> {{ message.role === 'user' ? 'You' : 'AI Assistant' }} </p> <div class="whitespace-pre-wrap">{{ getMessageText(message) }}</div> </div> <div ref="messagesEndRef" /> </div> <form @submit.prevent="handleSubmit" class="w-full flex items-center space-x-2 pt-2 border-t"> <UInput name="prompt" v-model="input" placeholder="Type your message..." class="flex-1" autocomplete="off" autofocus /> <UButton type="submit" color="primary" size="md" square> <UIcon name="i-lucide-send" class="w-5 h-5" /> </UButton> </form> </div> </template> `, type: "text" }, "examples/ai/web/react/next/src/app/ai/page.tsx": { metadata: { updatedAt: "2025-06-17T06:06:35.000Z", updatedHash: "0c366c76eb" }, content: `"use client" import { useChat } from "@ai-sdk/react"; import { Input } from "@/components/ui/input"; import { Button } from "@/components/ui/button"; import { Send } from "lucide-react"; import { useRef, useEffect } from "react"; export default function AIPage() { const { messages, input, handleInputChange, handleSubmit } = useChat({ api: \`\${process.env.NEXT_PUBLIC_SERVER_URL}/ai\`, }); const messagesEndRef = useRef<HTMLDivElement>(null); useEffect(() => { messagesEndRef.current?.scrollIntoView({ behavior: "smooth" }); }, [messages]); return ( <div className="grid grid-rows-[1fr_auto] overflow-hidden w-full mx-auto p-4"> <div className="overflow-y-auto space-y-4 pb-4"> {messages.length === 0 ? ( <div className="text-center text-muted-foreground mt-8"> Ask me anything to get started! </div> ) : ( messages.map((message) => ( <div key={message.id} className={\`p-3 rounded-lg \${ message.role === "user" ? "bg-primary/10 ml-8" : "bg-secondary/20 mr-8" }\`} > <p className="text-sm font-semibold mb-1"> {message.role === "user" ? "You" : "AI Assistant"} </p> <div className="whitespace-pre-wrap">{message.content}</div> </div> )) )} <div ref={messagesEndRef} /> </div> <form onSubmit={handleSubmit} className="w-full flex items-center space-x-2 pt-2 border-t" > <Input name="prompt" value={input} onChange={handleInputChange} placeholder="Type your message..." className="flex-1" autoComplete="off" autoFocus /> <Button type="submit" size="icon"> <Send size={18} /> </Button> </form> </div> ); } `, type: "text" }, "examples/ai/web/react/react-router/src/routes/ai.tsx": { metadata: { updatedAt: "2025-06-17T06:06:35.000Z", updatedHash: "47e1850967" }, content: `import { useChat } from "@ai-sdk/react"; import { Input } from "@/components/ui/input"; import { Button } from "@/components/ui/button"; import { Send } from "lucide-react"; import { useRef, useEffect } from "react"; export default function AI() { const { messages, input, handleInputChange, handleSubmit } = useChat({ api: \`\${import.meta.env.VITE_SERVER_URL}/ai\`, }); const messagesEndRef = useRef<HTMLDivElement>(null); useEffect(() => { messagesEndRef.current?.scrollIntoView({ behavior: "smooth" }); }, [messages]); return ( <div className="grid grid-rows-[1fr_auto] overflow-hidden w-full mx-auto p-4"> <div className="overflow-y-auto space-y-4 pb-4"> {messages.length === 0 ? ( <div className="text-center text-muted-foreground mt-8"> Ask me anything to get started! </div> ) : ( messages.map((message) => ( <div key={message.id} className={\`p-3 rounded-lg \${ message.role === "user" ? "bg-primary/10 ml-8" : "bg-secondary/20 mr-8" }\`} > <p className="text-sm font-semibold mb-1"> {message.role === "user" ? "You" : "AI Assistant"} </p> <div className="whitespace-pre-wrap">{message.content}</div> </div> )) )} <div ref={messagesEndRef} /> </div> <form onSubmit={handleSubmit} className="w-full flex items-center space-x-2 pt-2 border-t" > <Input name="prompt" value={input} onChange={handleInputChange} placeholder="Type your message..." className="flex-1" autoComplete="off" autoFocus /> <Button type="submit" size="icon"> <Send size={18} /> </Button> </form> </div> ); } `, type: "text" }, "examples/ai/web/react/tanstack-router/src/routes/ai.tsx": { metadata: { updatedAt: "2025-06-17T06:06:35.000Z", updatedHash: "64b5db59bb" }, content: `import { createFileRoute } from "@tanstack/react-router"; import { useChat } from "@ai-sdk/react"; import { Input } from "@/components/ui/input"; import { Button } from "@/components/ui/button"; import { Send } from "lucide-react"; import { useRef, useEffect } from "react"; export const Route = createFileRoute("/ai")({ component: RouteComponent, }); function RouteComponent() { const { messages, input, handleInputChange, handleSubmit } = useChat({ api: \`\${import.meta.env.VITE_SERVER_URL}/ai\`, }); const messagesEndRef = useRef<HTMLDivElement>(null); useEffect(() => { messagesEndRef.current?.scrollIntoView({ behavior: "smooth" }); }, [messages]); return ( <div className="grid grid-rows-[1fr_auto] overflow-hidden w-full mx-auto p-4"> <div className="overflow-y-auto space-y-4 pb-4"> {messages.length === 0 ? ( <div className="text-center text-muted-foreground mt-8"> Ask me anything to get started! </div> ) : ( messages.map((message) => ( <div key={message.id} className={\`p-3 rounded-lg \${ message.role === "user" ? "bg-primary/10 ml-8" : "bg-secondary/20 mr-8" }\`} > <p className="text-sm font-semibold mb-1"> {message.role === "user" ? "You" : "AI Assistant"} </p> <div className="whitespace-pre-wrap">{message.content}</div> </div> )) )} <div ref={messagesEndRef} /> </div> <form onSubmit={handleSubmit} className="w-full flex items-center space-x-2 pt-2 border-t" > <Input name="prompt" value={input} onChange={handleInputChange} placeholder="Type your message..." className="flex-1" autoComplete="off" autoFocus /> <Button type="submit" size="icon"> <Send size={18} /> </Button> </form> </div> ); } `, type: "text" }, "examples/ai/web/react/tanstack-start/src/routes/ai.tsx": { metadata: { updatedAt: "2025-06-17T06:06:35.000Z", updatedHash: "64b5db59bb" }, content: `import { createFileRoute } from "@tanstack/react-router"; import { useChat } from "@ai-sdk/react"; import { Input } from "@/components/ui/input"; import { Button } from "@/components/ui/button"; import { Send } from "lucide-react"; import { useRef, useEffect } from "react"; export const Route = createFileRoute("/ai")({ component: RouteComponent, }); function RouteComponent() { const { messages, input, handleInputChange, handleSubmit } = useChat({ api: \`\${import.meta.env.VITE_SERVER_URL}/ai\`, }); const messagesEndRef = useRef<HTMLDivElement>(null); useEffect(() => { messagesEndRef.current?.scrollIntoView({ behavior: "smooth" }); }, [messages]); return ( <div className="grid grid-rows-[1fr_auto] overflow-hidden w-full mx-auto p-4"> <div className="overflow-y-auto space-y-4 pb-4"> {messages.length === 0 ? ( <div className="text-center text-muted-foreground mt-8"> Ask me anything to get started! </div> ) : ( messages.map((message) => ( <div key={message.id} className={\`p-3 rounded-lg \${ message.role === "user" ? "bg-primary/10 ml-8" : "bg-secondary/20 mr-8" }\`} > <p className="text-sm font-semibold mb-1"> {message.role === "user" ? "You" : "AI Assistant"} </p> <div className="whitespace-pre-wrap">{message.content}</div> </div> )) )} <div ref={messagesEndRef} /> </div> <form onSubmit={handleSubmit} className="w-full flex items-center space-x-2 pt-2 border-t" > <Input name="prompt" value={input} onChange={handleInputChange} placeholder="Type your message..." className="flex-1" autoComplete="off" autoFocus /> <Button type="submit" size="icon"> <Send size={18} /> </Button> </form> </div> ); } `, type: "text" }, "examples/ai/web/svelte/src/routes/ai/+page.svelte": { metadata: { updatedAt: "2025-06-17T06:06:35.000Z", updatedHash: "a6b5e9020b" }, content: `<script lang="ts"> import { PUBLIC_SERVER_URL } from '$env/static/public'; import { Chat } from '@ai-sdk/svelte'; const chat = new Chat({ api: \`\${PUBLIC_SERVER_URL}/ai\`, }); let messagesEndElement: HTMLDivElement | null = null; $effect(() => { const messageCount = chat.messages.length; if (messageCount > 0) { setTimeout(() => { messagesEndElement?.scrollIntoView({ behavior: 'smooth' }); }, 0); } }); <\/script> <div class="mx-auto grid h-full w-full max-w-2xl grid-rows-[1fr_auto] overflow-hidden p-4"> <div class="mb-4 space-y-4 overflow-y-auto pb-4"> {#if chat.messages.length === 0} <div class="mt-8 text-center text-neutral-500">Ask the AI anything to get started!</div> {/if} {#each chat.messages as message (message.id)} <div class="w-fit max-w-[85%] rounded-lg p-3 text-sm md:text-base" class:ml-auto={message.role === 'user'} class:bg-indigo-600={message.role === 'user'} class:text-white={message.role === 'user'} class:bg-neutral-700={message.role === 'assistant'} class:text-neutral-100={message.role === 'assistant'} > <p class="mb-1 text-xs font-semibold uppercase tracking-wide" class:text-indigo-200={message.role === 'user'} class:text-neutral-400={message.role === 'assistant'} > {message.role === 'user' ? 'You' : 'AI Assistant'} </p> <div class="whitespace-pre-wrap break-words"> {#each message.parts as part, partIndex (partIndex)} {#if part.type === 'text'} {part.text} {:else if part.type === 'tool-invocation'} <pre class="mt-2 rounded bg-neutral-800 p-2 text-xs text-neutral-300" >{JSON.stringify(part.toolInvocation, null, 2)}</pre > {/if} {/each} </div> </div> {/each} <div bind:this={messagesEndElement}></div> </div> <form onsubmit={chat.handleSubmit} class="flex w-full items-center space-x-2 border-t border-neutral-700 pt-4" > <input name="prompt" bind:value={chat.input} placeholder="Type your message..." class="flex-1 rounded border border-neutral-600 bg-neutral-800 px-3 py-2 text-neutral-100 placeholder-neutral-500 focus:border-indigo-500 focus:outline-none focus:ring-1 focus:ring-indigo-500 disabled:opacity-50" autocomplete="off" onkeydown={(e) => { if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); chat.handleSubmit(e); } }} /> <button type="submit" disabled={!chat.input.trim()} class="inline-flex h-10 w-10 items-center justify-center rounded bg-indigo-600 text-white hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2 focus:ring-offset-neutral-900 disabled:cursor-not-allowed disabled:opacity-50" aria-label="Send message" > <svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" > <path d="m22 2-7 20-4-9-9-4Z" /><path d="M22 2 11 13" /> </svg> </button> </form> </div> `, type: "text" }, "examples/todo/native/nativewind/app/(drawer)/todos.tsx.hbs": { metadata: { updatedAt: "2025-06-17T06:06:35.000Z", updatedHash: "2bb32caaba" }, content: `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"; import type { Id } from "@{{projectName}}/backend/convex/_generated/dataModel"; {{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> ); } `, type: "text" }, "examples/todo/native/unistyles/app/(drawer)/todos.tsx.hbs": { metadata: { updatedAt: "2025-06-17T06:06:35.000Z", updatedHash: "26a877bbb6" }, content: `import { useState } from "react"; import { View, Text, TextInput, TouchableOpacity, ScrollView, ActivityIndicator, Alert, } from "react-native"; import { Ionicons } from "@expo/vector-icons"; import { StyleSheet, useUnistyles } from "react-native-unistyles"; {{#if (eq backend "convex")}} import { useMutation, useQuery } from "convex/react"; import { api } from "@{{projectName}}/backend/convex/_generated/api"; import type { Id } from "@{{projectName}}/backend/convex/_generated/dataModel"; {{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(""); const { theme } = useUnistyles(); {{#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}} const isLoading = {{#if (eq backend "convex")}}!todos{{else}}todos.isLoading{{/if}}; const isCreating = {{#if (eq backend "convex")}}false{{else}}createMutation.isPending{{/if}}; const primaryButtonTextColor = theme.colors.background; return ( <Container> <ScrollView style={styles.scrollView}> <View style={styles.headerContainer}> <Text style={styles.headerTitle}>Todo List</Text> <Text style={styles.headerSubtitle}> Manage your tasks efficiently </Text> <View style={styles.inputContainer}> <TextInput value={newTodoText} onChangeText={setNewTodoText} placeholder="Add a new task..." placeholderTextColor={theme.colors.border} editable={!isCreating} style={styles.textInput} onSubmitEditing={handleAddTodo} returnKeyType="done" /> <TouchableOpacity onPress={handleAddTodo} disabled={isCreating || !newTodoText.trim()} style={[ styles.addButton, (isCreating || !newTodoText.trim()) && styles.addButtonDisabled, ]} > {isCreating ? ( <ActivityIndicator size="small" color={primaryButtonTextColor} /> ) : ( <Ionicons name="add" size={24} color={primaryButtonTextColor} /> )} </TouchableOpacity> </View> </View> {isLoading && ( <View style={styles.loadingContainer}> <ActivityIndicator size="large" color={theme.colors.primary} /> <Text style={styles.loadingText}>Loading todos...</Text> </View> )} {{#if (eq backend "convex")}} {todos && todos.length === 0 && !isLoading && ( <Text style={styles.emptyText}>No todos yet. Add one!</Text> )} {todos?.map((todo) => ( <View key={todo._id} style={styles.todoItem}> <TouchableOpacity onPress={() => handleToggleTodo(todo._id, todo.completed)} style={styles.todoContent} > <Ionicons name={todo.completed ? "checkbox" : "square-outline"} size={24} color={todo.completed ? theme.colors.primary : theme.colors.typography} style={styles.checkbox} /> <Text style={[ styles.todoText, todo.completed && styles.todoTextCompleted, ]} > {todo.text} </Text> </TouchableOpacity> <TouchableOpacity onPress={() => handleDeleteTodo(todo._id)}> <Ionicons name="trash-outline" size={24} color={theme.colors.destructive} /> </TouchableOpacity> </View> ))} {{else}} {todos.data && todos.data.length === 0 && !isLoading && ( <Text style={styles.emptyText}>No todos yet. Add one!</Text> )} {todos.data?.map((todo: { id: number; text: string; completed: boolean }) => ( <View key={todo.id} style={styles.todoItem}> <TouchableOpacity onPress={() => handleToggleTodo(todo.id, todo.completed)} style={styles.todoContent} > <Ionicons name={todo.completed ? "checkbox" : "square-outline"} size={24} color={todo.completed ? theme.colors.primary : theme.colors.typography} style={styles.checkbox} /> <Text style={[ styles.todoText, todo.completed && styles.todoTextCompleted, ]} > {todo.text} </Text> </TouchableOpacity> <TouchableOpacity onPress={() => handleDeleteTodo(todo.id)}> <Ionicons name="trash-outline" size={24} color={theme.colors.destructive} /> </TouchableOpacity> </View> ))} {{/if}} </ScrollView> </Container> ); } const styles = StyleSheet.create((theme) => ({ scrollView: { flex: 1, }, headerContainer: { paddingHorizontal: theme.spacing.md, paddingVertical: theme.spacing.lg, borderBottomWidth: 1, borderBottomColor: theme.colors.border, backgroundColor: theme.colors.background, }, headerTitle: { fontSize: 28, fontWeight: "bold", color: theme.colors.typography, marginBottom: theme.spacing.sm, }, headerSubtitle: { fontSize: 16, color: theme.colors.typography, marginBottom: theme.spacing.md, }, inputContainer: { flexDirection: "row", alignItems: "center", marginBottom: theme.spacing.md, }, textInput: { flex: 1, borderWidth: 1, borderColor: theme.colors.border, borderRadius: 8, paddingHorizontal: theme.spacing.md, paddingVertical: theme.spacing.sm, color: theme.colors.typography, backgroundColor: theme.colors.background, marginRight: theme.spacing.sm, fontSize: 16, }, addButton: { backgroundColor: theme.colors.primary, padding: theme.spacing.sm, borderRadius: 8, justifyContent: "center", alignItems: "center", }, addButtonDisabled: { backgroundColor: theme.colors.border, }, loadingContainer: { flex: 1, justifyContent: "center", alignItems: "center", padding: theme.spacing.lg, }, loadingText: { marginTop: theme.spacing.sm, fontSize: 16, color: theme.colors.typography, }, emptyText: { textAlign: "center", marginTop: theme.spacing.xl, fontSize: 16, color: theme.colors.typography, }, todoItem: { flexDirection: "row", justifyContent