Skip to content

Commit 39a0374

Browse files
Add Todo feature with UI and backend integration
1 parent 1ca76b2 commit 39a0374

File tree

13 files changed

+472
-60
lines changed

13 files changed

+472
-60
lines changed

apps/cli/template/base/packages/client/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
},
2525
"dependencies": {
2626
"@hookform/resolvers": "^3.10.0",
27+
"@radix-ui/react-checkbox": "^1.1.4",
2728
"@radix-ui/react-dropdown-menu": "^2.1.6",
2829
"@radix-ui/react-label": "^2.1.2",
2930
"@radix-ui/react-slot": "^1.1.2",

apps/cli/template/base/packages/client/src/components/header.tsx

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,15 @@ export default function Header() {
1515
>
1616
Home
1717
</Link>
18+
<Link
19+
to="/todos"
20+
activeProps={{
21+
className: "font-bold",
22+
}}
23+
activeOptions={{ exact: true }}
24+
>
25+
Todos
26+
</Link>
1827
</div>
1928
<div className="flex flex-row items-center gap-2">
2029
<ModeToggle />
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
import * as React from "react"
2+
3+
import { cn } from "@/lib/utils"
4+
5+
function Card({ className, ...props }: React.ComponentProps<"div">) {
6+
return (
7+
<div
8+
data-slot="card"
9+
className={cn(
10+
"bg-card text-card-foreground flex flex-col gap-6 rounded-xl border py-6 shadow-sm",
11+
className
12+
)}
13+
{...props}
14+
/>
15+
)
16+
}
17+
18+
function CardHeader({ className, ...props }: React.ComponentProps<"div">) {
19+
return (
20+
<div
21+
data-slot="card-header"
22+
className={cn(
23+
"@container/card-header grid auto-rows-min grid-rows-[auto_auto] items-start gap-1.5 px-6 has-data-[slot=card-action]:grid-cols-[1fr_auto] [.border-b]:pb-6",
24+
className
25+
)}
26+
{...props}
27+
/>
28+
)
29+
}
30+
31+
function CardTitle({ className, ...props }: React.ComponentProps<"div">) {
32+
return (
33+
<div
34+
data-slot="card-title"
35+
className={cn("leading-none font-semibold", className)}
36+
{...props}
37+
/>
38+
)
39+
}
40+
41+
function CardDescription({ className, ...props }: React.ComponentProps<"div">) {
42+
return (
43+
<div
44+
data-slot="card-description"
45+
className={cn("text-muted-foreground text-sm", className)}
46+
{...props}
47+
/>
48+
)
49+
}
50+
51+
function CardAction({ className, ...props }: React.ComponentProps<"div">) {
52+
return (
53+
<div
54+
data-slot="card-action"
55+
className={cn(
56+
"col-start-2 row-span-2 row-start-1 self-start justify-self-end",
57+
className
58+
)}
59+
{...props}
60+
/>
61+
)
62+
}
63+
64+
function CardContent({ className, ...props }: React.ComponentProps<"div">) {
65+
return (
66+
<div
67+
data-slot="card-content"
68+
className={cn("px-6", className)}
69+
{...props}
70+
/>
71+
)
72+
}
73+
74+
function CardFooter({ className, ...props }: React.ComponentProps<"div">) {
75+
return (
76+
<div
77+
data-slot="card-footer"
78+
className={cn("flex items-center px-6 [.border-t]:pt-6", className)}
79+
{...props}
80+
/>
81+
)
82+
}
83+
84+
export {
85+
Card,
86+
CardHeader,
87+
CardFooter,
88+
CardTitle,
89+
CardAction,
90+
CardDescription,
91+
CardContent,
92+
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import * as React from "react"
2+
import * as CheckboxPrimitive from "@radix-ui/react-checkbox"
3+
import { CheckIcon } from "lucide-react"
4+
5+
import { cn } from "@/lib/utils"
6+
7+
function Checkbox({
8+
className,
9+
...props
10+
}: React.ComponentProps<typeof CheckboxPrimitive.Root>) {
11+
return (
12+
<CheckboxPrimitive.Root
13+
data-slot="checkbox"
14+
className={cn(
15+
"peer border-input dark:bg-input/30 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground dark:data-[state=checked]:bg-primary data-[state=checked]:border-primary focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive size-4 shrink-0 rounded-[4px] border shadow-xs transition-shadow outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50",
16+
className
17+
)}
18+
{...props}
19+
>
20+
<CheckboxPrimitive.Indicator
21+
data-slot="checkbox-indicator"
22+
className="flex items-center justify-center text-current transition-none"
23+
>
24+
<CheckIcon className="size-3.5" />
25+
</CheckboxPrimitive.Indicator>
26+
</CheckboxPrimitive.Root>
27+
)
28+
}
29+
30+
export { Checkbox }
Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
import { Button } from "@/components/ui/button";
2+
import {
3+
Card,
4+
CardContent,
5+
CardDescription,
6+
CardHeader,
7+
CardTitle,
8+
} from "@/components/ui/card";
9+
import { Checkbox } from "@/components/ui/checkbox";
10+
import { Input } from "@/components/ui/input";
11+
import { trpc } from "@/utils/trpc";
12+
import { createFileRoute } from "@tanstack/react-router";
13+
import { Loader2, Trash2 } from "lucide-react";
14+
import { useState } from "react";
15+
16+
export const Route = createFileRoute("/todos")({
17+
component: TodosRoute,
18+
});
19+
20+
function TodosRoute() {
21+
const [newTodoText, setNewTodoText] = useState("");
22+
23+
const todos = trpc.todo.getAll.useQuery();
24+
const createMutation = trpc.todo.create.useMutation({
25+
onSuccess: () => {
26+
todos.refetch();
27+
setNewTodoText("");
28+
},
29+
});
30+
const toggleMutation = trpc.todo.toggle.useMutation({
31+
onSuccess: () => todos.refetch(),
32+
});
33+
const deleteMutation = trpc.todo.delete.useMutation({
34+
onSuccess: () => todos.refetch(),
35+
});
36+
37+
const handleAddTodo = (e: React.FormEvent) => {
38+
e.preventDefault();
39+
if (newTodoText.trim()) {
40+
createMutation.mutate({ text: newTodoText });
41+
}
42+
};
43+
44+
const handleToggleTodo = (id: number, completed: boolean) => {
45+
toggleMutation.mutate({ id, completed: !completed });
46+
};
47+
48+
const handleDeleteTodo = (id: number) => {
49+
deleteMutation.mutate({ id });
50+
};
51+
52+
return (
53+
<div className="container mx-auto max-w-md py-10">
54+
<Card>
55+
<CardHeader>
56+
<CardTitle>Todo List</CardTitle>
57+
<CardDescription>Manage your tasks efficiently</CardDescription>
58+
</CardHeader>
59+
<CardContent>
60+
<form
61+
onSubmit={handleAddTodo}
62+
className="mb-6 flex items-center space-x-2"
63+
>
64+
<Input
65+
value={newTodoText}
66+
onChange={(e) => setNewTodoText(e.target.value)}
67+
placeholder="Add a new task..."
68+
disabled={createMutation.isPending}
69+
/>
70+
<Button
71+
type="submit"
72+
disabled={createMutation.isPending || !newTodoText.trim()}
73+
>
74+
{createMutation.isPending ? (
75+
<Loader2 className="h-4 w-4 animate-spin" />
76+
) : (
77+
"Add"
78+
)}
79+
</Button>
80+
</form>
81+
82+
{todos.isLoading ? (
83+
<div className="flex justify-center py-4">
84+
<Loader2 className="h-6 w-6 animate-spin" />
85+
</div>
86+
) : todos.data?.length === 0 ? (
87+
<p className="py-4 text-center text-muted-foreground">
88+
No todos yet. Add one above!
89+
</p>
90+
) : (
91+
<ul className="space-y-2">
92+
{todos.data?.map((todo) => (
93+
<li
94+
key={todo.id}
95+
className="flex items-center justify-between rounded-md border p-2"
96+
>
97+
<div className="flex items-center space-x-2">
98+
<Checkbox
99+
checked={todo.completed}
100+
onCheckedChange={() =>
101+
handleToggleTodo(todo.id, todo.completed)
102+
}
103+
id={`todo-${todo.id}`}
104+
/>
105+
<label
106+
htmlFor={`todo-${todo.id}`}
107+
className={`${todo.completed ? "text-muted-foreground line-through" : ""}`}
108+
>
109+
{todo.text}
110+
</label>
111+
</div>
112+
<Button
113+
variant="ghost"
114+
size="icon"
115+
onClick={() => handleDeleteTodo(todo.id)}
116+
aria-label="Delete todo"
117+
>
118+
<Trash2 className="h-4 w-4" />
119+
</Button>
120+
</li>
121+
))}
122+
</ul>
123+
)}
124+
</CardContent>
125+
</Card>
126+
</div>
127+
);
128+
}
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
import { router, publicProcedure } from "../lib/trpc";
2+
import { todoRouter } from "./todo";
23

34
export const appRouter = router({
45
healthCheck: publicProcedure.query(() => {
56
return "OK";
67
}),
8+
todo: todoRouter,
79
});
810

911
export type AppRouter = typeof appRouter;

apps/cli/template/with-auth/packages/client/src/components/header.tsx

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,15 @@ export default function Header() {
1616
>
1717
Home
1818
</Link>
19+
<Link
20+
to="/todos"
21+
activeProps={{
22+
className: "font-bold",
23+
}}
24+
activeOptions={{ exact: true }}
25+
>
26+
Todos
27+
</Link>
1928
<Link
2029
to="/dashboard"
2130
activeProps={{

apps/cli/template/with-auth/packages/server/src/routers/index.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11

22
import { router, publicProcedure, protectedProcedure } from "../lib/trpc";
3+
import { todoRouter } from "./todo";
4+
35

46
export const appRouter = router({
57
healthCheck: publicProcedure.query(() => {
@@ -11,6 +13,7 @@ export const appRouter = router({
1113
user: ctx.session.user,
1214
};
1315
}),
16+
todo: todoRouter,
1417
});
1518

1619
export type AppRouter = typeof appRouter;
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import { z } from "zod";
2+
import { router, publicProcedure } from "../lib/trpc";
3+
import { todo } from "../db/schema";
4+
import { eq } from "drizzle-orm";
5+
import { db } from "../db";
6+
7+
export const todoRouter = router({
8+
getAll: publicProcedure.query(async () => {
9+
return await db.select().from(todo).all();
10+
}),
11+
12+
create: publicProcedure
13+
.input(z.object({ text: z.string().min(1) }))
14+
.mutation(async ({ input }) => {
15+
return await db
16+
.insert(todo)
17+
.values({
18+
text: input.text,
19+
})
20+
.returning()
21+
.get();
22+
}),
23+
24+
toggle: publicProcedure
25+
.input(z.object({ id: z.number(), completed: z.boolean() }))
26+
.mutation(async ({ input }) => {
27+
return await db
28+
.update(todo)
29+
.set({ completed: input.completed })
30+
.where(eq(todo.id, input.id))
31+
.returning()
32+
.get();
33+
}),
34+
35+
delete: publicProcedure
36+
.input(z.object({ id: z.number() }))
37+
.mutation(async ({ input }) => {
38+
return await db
39+
.delete(todo)
40+
.where(eq(todo.id, input.id))
41+
.returning()
42+
.get();
43+
}),
44+
});

0 commit comments

Comments
 (0)