From 9e00e20880d66239afd3cb83da6e911bb562d212 Mon Sep 17 00:00:00 2001 From: Aman Varshney Date: Wed, 30 Apr 2025 23:09:56 +0530 Subject: [PATCH] add todo, ai template for next --- .changeset/curvy-jobs-unite.md | 5 + apps/cli/src/helpers/template-manager.ts | 14 ++ apps/cli/src/index.ts | 2 - .../ai/server/next/src/app/ai/route.ts | 15 ++ .../ai/web/react/next/src/app/ai/page.tsx | 67 ++++++++ .../web/react/next/src/app/todos/page.tsx.hbs | 162 ++++++++++++++++++ 6 files changed, 263 insertions(+), 2 deletions(-) create mode 100644 .changeset/curvy-jobs-unite.md create mode 100644 apps/cli/templates/examples/ai/server/next/src/app/ai/route.ts create mode 100644 apps/cli/templates/examples/ai/web/react/next/src/app/ai/page.tsx create mode 100644 apps/cli/templates/examples/todo/web/react/next/src/app/todos/page.tsx.hbs diff --git a/.changeset/curvy-jobs-unite.md b/.changeset/curvy-jobs-unite.md new file mode 100644 index 0000000..65fe9ac --- /dev/null +++ b/.changeset/curvy-jobs-unite.md @@ -0,0 +1,5 @@ +--- +"create-better-t-stack": patch +--- + +add todo, ai template for next diff --git a/apps/cli/src/helpers/template-manager.ts b/apps/cli/src/helpers/template-manager.ts index b285a94..e1767ab 100644 --- a/apps/cli/src/helpers/template-manager.ts +++ b/apps/cli/src/helpers/template-manager.ts @@ -494,6 +494,20 @@ export async function setupExamplesTemplate( const exampleBaseDir = path.join(PKG_ROOT, `templates/examples/${example}`); + if (example === "ai" && context.backend === "next" && serverAppDirExists) { + const aiNextServerSrc = path.join(exampleBaseDir, "server/next"); + + if (await fs.pathExists(aiNextServerSrc)) { + await processAndCopyFiles( + "**/*", + aiNextServerSrc, + serverAppDir, + context, + false, + ); + } + } + if (serverAppDirExists) { const exampleServerSrc = path.join(exampleBaseDir, "server"); if (await fs.pathExists(exampleServerSrc)) { diff --git a/apps/cli/src/index.ts b/apps/cli/src/index.ts index ad02e0c..b0713e8 100644 --- a/apps/cli/src/index.ts +++ b/apps/cli/src/index.ts @@ -353,8 +353,6 @@ function processAndValidateFlags( ); if (config.backend !== "convex" && options.examples.includes("none")) { config.examples = []; - } else { - config.examples = ["todo"]; } } } diff --git a/apps/cli/templates/examples/ai/server/next/src/app/ai/route.ts b/apps/cli/templates/examples/ai/server/next/src/app/ai/route.ts new file mode 100644 index 0000000..74482ed --- /dev/null +++ b/apps/cli/templates/examples/ai/server/next/src/app/ai/route.ts @@ -0,0 +1,15 @@ +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(); +} diff --git a/apps/cli/templates/examples/ai/web/react/next/src/app/ai/page.tsx b/apps/cli/templates/examples/ai/web/react/next/src/app/ai/page.tsx new file mode 100644 index 0000000..69d81f2 --- /dev/null +++ b/apps/cli/templates/examples/ai/web/react/next/src/app/ai/page.tsx @@ -0,0 +1,67 @@ +"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(null); + + useEffect(() => { + messagesEndRef.current?.scrollIntoView({ behavior: "smooth" }); + }, [messages]); + + return ( +
+
+ {messages.length === 0 ? ( +
+ Ask me anything to get started! +
+ ) : ( + messages.map((message) => ( +
+

+ {message.role === "user" ? "You" : "AI Assistant"} +

+
{message.content}
+
+ )) + )} +
+
+ +
+ + +
+
+ ); +} diff --git a/apps/cli/templates/examples/todo/web/react/next/src/app/todos/page.tsx.hbs b/apps/cli/templates/examples/todo/web/react/next/src/app/todos/page.tsx.hbs new file mode 100644 index 0000000..662beb4 --- /dev/null +++ b/apps/cli/templates/examples/todo/web/react/next/src/app/todos/page.tsx.hbs @@ -0,0 +1,162 @@ +"use client" + +import { Button } from "@/components/ui/button"; +import { + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, +} from "@/components/ui/card"; +import { Checkbox } from "@/components/ui/checkbox"; +import { Input } from "@/components/ui/input"; +import { Loader2, Trash2 } from "lucide-react"; +import { useState } from "react"; +import { useMutation, useQuery } from "@tanstack/react-query"; + +{{#if (eq api "orpc")}} +import { orpc } from "@/utils/orpc"; +{{/if}} +{{#if (eq api "trpc")}} +import { trpc } from "@/utils/trpc"; +{{/if}} + + +export default function TodosPage() { + const [newTodoText, setNewTodoText] = useState(""); + + {{#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 = (e: React.FormEvent) => { + e.preventDefault(); + if (newTodoText.trim()) { + createMutation.mutate({ text: newTodoText }); + } + }; + + const handleToggleTodo = (id: number, completed: boolean) => { + toggleMutation.mutate({ id, completed: !completed }); + }; + + const handleDeleteTodo = (id: number) => { + deleteMutation.mutate({ id }); + }; + + return ( +
+ + + Todo List + Manage your tasks efficiently + + +
+ setNewTodoText(e.target.value)} + placeholder="Add a new task..." + disabled={createMutation.isPending} + /> + +
+ + {todos.isLoading ? ( +
+ +
+ ) : todos.data?.length === 0 ? ( +

+ No todos yet. Add one above! +

+ ) : ( +
    + {todos.data?.map((todo) => ( +
  • +
    + + handleToggleTodo(todo.id, todo.completed) + } + id={`todo-${todo.id}`} + /> + +
    + +
  • + ))} +
+ )} +
+
+
+ ); +}