add convex

This commit is contained in:
Aman Varshney
2025-04-28 11:42:11 +05:30
parent 7ef3cfce9e
commit 2a5358a105
70 changed files with 2330 additions and 1139 deletions

View File

@@ -8,61 +8,90 @@ import {
} from "@/components/ui/card";
import { Checkbox } from "@/components/ui/checkbox";
import { Input } from "@/components/ui/input";
{{#if (eq api "orpc")}}
import { orpc } from "@/utils/orpc";
{{/if}}
{{#if (eq api "trpc")}}
import { trpc } from "@/utils/trpc";
{{/if}}
import { useMutation, useQuery } from "@tanstack/react-query";
import { Loader2, Trash2 } from "lucide-react";
import { useState } from "react";
{{#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}}
{{#if (eq api "orpc")}}
import { orpc } from "@/utils/orpc";
{{/if}}
{{#if (eq api "trpc")}}
import { trpc } from "@/utils/trpc";
{{/if}}
import { useMutation, useQuery } from "@tanstack/react-query";
{{/if}}
export default function Todos() {
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}}
{{#if (eq backend "convex")}}
const todos = useQuery(api.todos.getAll);
const createTodo = useMutation(api.todos.create);
const toggleTodo = useMutation(api.todos.toggle);
const deleteTodo = useMutation(api.todos.deleteTodo);
const handleAddTodo = async (e: React.FormEvent) => {
e.preventDefault();
const text = newTodoText.trim();
if (!text) return;
await createTodo({ text });
setNewTodoText("");
};
const handleToggleTodo = (id: Id<"todos">, currentCompleted: boolean) => {
toggleTodo({ id, completed: !currentCompleted });
};
const handleDeleteTodo = (id: Id<"todos">) => {
deleteTodo({ 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 = (e: React.FormEvent) => {
e.preventDefault();
@@ -78,6 +107,7 @@ export default function Todos() {
const handleDeleteTodo = (id: number) => {
deleteMutation.mutate({ id });
};
{{/if}}
return (
<div className="w-full mx-auto max-w-md py-10">
@@ -95,62 +125,117 @@ export default function Todos() {
value={newTodoText}
onChange={(e) => setNewTodoText(e.target.value)}
placeholder="Add a new task..."
{{#if (eq backend "convex")}}
{{!-- Convex mutations don't have an easy isPending state here, disable based on text --}}
{{else}}
disabled={createMutation.isPending}
{{/if}}
/>
<Button
type="submit"
{{#if (eq backend "convex")}}
disabled={!newTodoText.trim()}
{{else}}
disabled={createMutation.isPending || !newTodoText.trim()}
{{/if}}
>
{createMutation.isPending ? (
<Loader2 className="h-4 w-4 animate-spin" />
) : (
"Add"
)}
{{#if (eq backend "convex")}}
Add
{{else}}
{createMutation.isPending ? (
<Loader2 className="h-4 w-4 animate-spin" />
) : (
"Add"
)}
{{/if}}
</Button>
</form>
{todos.isLoading ? (
<div className="flex justify-center py-4">
<Loader2 className="h-6 w-6 animate-spin" />
</div>
) : todos.data?.length === 0 ? (
<p className="py-4 text-center">
No todos yet. Add one above!
</p>
) : (
<ul className="space-y-2">
{todos.data?.map((todo) => (
<li
key={todo.id}
className="flex items-center justify-between rounded-md border p-2"
>
<div className="flex items-center space-x-2">
<Checkbox
checked={todo.completed}
onCheckedChange={() =>
handleToggleTodo(todo.id, todo.completed)
}
id={`todo-${todo.id}`}
/>
<label
htmlFor={`todo-${todo.id}`}
className={`${todo.completed ? "line-through" : ""}`}
>
{todo.text}
</label>
</div>
<Button
variant="ghost"
size="icon"
onClick={() => handleDeleteTodo(todo.id)}
aria-label="Delete todo"
{{#if (eq backend "convex")}}
{todos === undefined ? (
<div className="flex justify-center py-4">
<Loader2 className="h-6 w-6 animate-spin" />
</div>
) : todos.length === 0 ? (
<p className="py-4 text-center">No todos yet. Add one above!</p>
) : (
<ul className="space-y-2">
{todos.map((todo) => (
<li
key={todo._id}
className="flex items-center justify-between rounded-md border p-2"
>
<Trash2 className="h-4 w-4" />
</Button>
</li>
))}
</ul>
)}
<div className="flex items-center space-x-2">
<Checkbox
checked={todo.completed}
onCheckedChange={() =>
handleToggleTodo(todo._id, todo.completed)
}
id={`todo-${todo._id}`}
/>
<label
htmlFor={`todo-${todo._id}`}
className={`${todo.completed ? "line-through text-muted-foreground" : ""}`}
>
{todo.text}
</label>
</div>
<Button
variant="ghost"
size="icon"
onClick={() => handleDeleteTodo(todo._id)}
aria-label="Delete todo"
>
<Trash2 className="h-4 w-4" />
</Button>
</li>
))}
</ul>
)}
{{else}}
{todos.isLoading ? (
<div className="flex justify-center py-4">
<Loader2 className="h-6 w-6 animate-spin" />
</div>
) : todos.data?.length === 0 ? (
<p className="py-4 text-center">
No todos yet. Add one above!
</p>
) : (
<ul className="space-y-2">
{todos.data?.map((todo) => (
<li
key={todo.id}
className="flex items-center justify-between rounded-md border p-2"
>
<div className="flex items-center space-x-2">
<Checkbox
checked={todo.completed}
onCheckedChange={() =>
handleToggleTodo(todo.id, todo.completed)
}
id={`todo-${todo.id}`}
/>
<label
htmlFor={`todo-${todo.id}`}
className={`${todo.completed ? "line-through" : ""}`}
>
{todo.text}
</label>
</div>
<Button
variant="ghost"
size="icon"
onClick={() => handleDeleteTodo(todo.id)}
aria-label="Delete todo"
>
<Trash2 className="h-4 w-4" />
</Button>
</li>
))}
</ul>
)}
{{/if}}
</CardContent>
</Card>
</div>

View File

@@ -8,17 +8,24 @@ import {
} from "@/components/ui/card";
import { Checkbox } from "@/components/ui/checkbox";
import { Input } from "@/components/ui/input";
{{#if (eq api "orpc")}}
import { orpc } from "@/utils/orpc";
{{/if}}
{{#if (eq api "trpc")}}
import { trpc } from "@/utils/trpc";
{{/if}}
import { useMutation, useQuery } from "@tanstack/react-query";
import { createFileRoute } from "@tanstack/react-router";
import { Loader2, Trash2 } from "lucide-react";
import { useState } from "react";
{{#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}}
{{#if (eq api "orpc")}}
import { orpc } from "@/utils/orpc";
{{/if}}
{{#if (eq api "trpc")}}
import { trpc } from "@/utils/trpc";
{{/if}}
import { useMutation, useQuery } from "@tanstack/react-query";
{{/if}}
export const Route = createFileRoute("/todos")({
component: TodosRoute,
});
@@ -26,48 +33,70 @@ export const Route = createFileRoute("/todos")({
function TodosRoute() {
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}}
{{#if (eq backend "convex")}}
const todos = useQuery(api.todos.getAll);
const createTodo = useMutation(api.todos.create);
const toggleTodo = useMutation(api.todos.toggle);
const deleteTodo = useMutation(api.todos.deleteTodo);
const handleAddTodo = async (e: React.FormEvent) => {
e.preventDefault();
const text = newTodoText.trim();
if (!text) return;
await createTodo({ text });
setNewTodoText("");
};
const handleToggleTodo = (id: Id<"todos">, currentCompleted: boolean) => {
toggleTodo({ id, completed: !currentCompleted });
};
const handleDeleteTodo = (id: Id<"todos">) => {
deleteTodo({ 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 = (e: React.FormEvent) => {
e.preventDefault();
@@ -83,6 +112,7 @@ function TodosRoute() {
const handleDeleteTodo = (id: number) => {
deleteMutation.mutate({ id });
};
{{/if}}
return (
<div className="mx-auto w-full max-w-md py-10">
@@ -100,60 +130,116 @@ function TodosRoute() {
value={newTodoText}
onChange={(e) => setNewTodoText(e.target.value)}
placeholder="Add a new task..."
{{#if (eq backend "convex")}}
{{else}}
disabled={createMutation.isPending}
{{/if}}
/>
<Button
type="submit"
{{#if (eq backend "convex")}}
disabled={!newTodoText.trim()}
{{else}}
disabled={createMutation.isPending || !newTodoText.trim()}
{{/if}}
>
{createMutation.isPending ? (
<Loader2 className="h-4 w-4 animate-spin" />
) : (
"Add"
)}
{{#if (eq backend "convex")}}
Add
{{else}}
{createMutation.isPending ? (
<Loader2 className="h-4 w-4 animate-spin" />
) : (
"Add"
)}
{{/if}}
</Button>
</form>
{todos.isLoading ? (
<div className="flex justify-center py-4">
<Loader2 className="h-6 w-6 animate-spin" />
</div>
) : todos.data?.length === 0 ? (
<p className="py-4 text-center">No todos yet. Add one above!</p>
) : (
<ul className="space-y-2">
{todos.data?.map((todo) => (
<li
key={todo.id}
className="flex items-center justify-between rounded-md border p-2"
>
<div className="flex items-center space-x-2">
<Checkbox
checked={todo.completed}
onCheckedChange={() =>
handleToggleTodo(todo.id, todo.completed)
}
id={`todo-${todo.id}`}
/>
<label
htmlFor={`todo-${todo.id}`}
className={`${todo.completed ? "line-through" : ""}`}
>
{todo.text}
</label>
</div>
<Button
variant="ghost"
size="icon"
onClick={() => handleDeleteTodo(todo.id)}
aria-label="Delete todo"
{{#if (eq backend "convex")}}
{todos === undefined ? (
<div className="flex justify-center py-4">
<Loader2 className="h-6 w-6 animate-spin" />
</div>
) : todos.length === 0 ? (
<p className="py-4 text-center">No todos yet. Add one above!</p>
) : (
<ul className="space-y-2">
{todos.map((todo) => (
<li
key={todo._id}
className="flex items-center justify-between rounded-md border p-2"
>
<Trash2 className="h-4 w-4" />
</Button>
</li>
))}
</ul>
)}
<div className="flex items-center space-x-2">
<Checkbox
checked={todo.completed}
onCheckedChange={() =>
handleToggleTodo(todo._id, todo.completed)
}
id={`todo-${todo._id}`}
/>
<label
htmlFor={`todo-${todo._id}`}
className={`${todo.completed ? "line-through text-muted-foreground" : ""}`}
>
{todo.text}
</label>
</div>
<Button
variant="ghost"
size="icon"
onClick={() => handleDeleteTodo(todo._id)}
aria-label="Delete todo"
>
<Trash2 className="h-4 w-4" />
</Button>
</li>
))}
</ul>
)}
{{else}}
{todos.isLoading ? (
<div className="flex justify-center py-4">
<Loader2 className="h-6 w-6 animate-spin" />
</div>
) : todos.data?.length === 0 ? (
<p className="py-4 text-center">
No todos yet. Add one above!
</p>
) : (
<ul className="space-y-2">
{todos.data?.map((todo) => (
<li
key={todo.id}
className="flex items-center justify-between rounded-md border p-2"
>
<div className="flex items-center space-x-2">
<Checkbox
checked={todo.completed}
onCheckedChange={() =>
handleToggleTodo(todo.id, todo.completed)
}
id={`todo-${todo.id}`}
/>
<label
htmlFor={`todo-${todo.id}`}
className={`${todo.completed ? "line-through" : ""}`}
>
{todo.text}
</label>
</div>
<Button
variant="ghost"
size="icon"
onClick={() => handleDeleteTodo(todo.id)}
aria-label="Delete todo"
>
<Trash2 className="h-4 w-4" />
</Button>
</li>
))}
</ul>
)}
{{/if}}
</CardContent>
</Card>
</div>

View File

@@ -8,32 +8,79 @@ import {
} from "@/components/ui/card";
import { Checkbox } from "@/components/ui/checkbox";
import { Input } from "@/components/ui/input";
{{#if (eq api "trpc")}}
import { useTRPC } from "@/utils/trpc";
{{/if}}
{{#if (eq api "orpc")}}
import { useORPC } from "@/utils/orpc";
{{/if}}
import { useMutation, useQuery } from "@tanstack/react-query";
import { createFileRoute } from "@tanstack/react-router";
import { Loader2, Trash2 } from "lucide-react";
import { useState } from "react";
{{#if (eq backend "convex")}}
import { useSuspenseQuery } from "@tanstack/react-query";
import { convexQuery } from "@convex-dev/react-query";
import { useMutation } from "convex/react";
import { api } from "@{{projectName}}/backend/convex/_generated/api.js";
import type { Id } from "@{{projectName}}/backend/convex/_generated/dataModel.js";
{{else}}
{{#if (eq api "trpc")}}
import { useTRPC } from "@/utils/trpc";
{{/if}}
{{#if (eq api "orpc")}}
import { useORPC } from "@/utils/orpc";
{{/if}}
import { useMutation, useQuery } from "@tanstack/react-query";
{{/if}}
export const Route = createFileRoute("/todos")({
component: TodosRoute,
});
function TodosRoute() {
{{#if (eq api "trpc")}}
const trpc = useTRPC();
{{/if}}
{{#if (eq api "orpc")}}
const orpc = useORPC();
{{/if}}
const [newTodoText, setNewTodoText] = useState("");
{{#if (eq api "trpc")}}
{{#if (eq backend "convex")}}
const todosQuery = useSuspenseQuery(convexQuery(api.todos.getAll, {}));
const todos = todosQuery.data;
const createTodo = useMutation(api.todos.create);
const toggleTodo = useMutation(api.todos.toggle);
const removeTodo = useMutation(api.todos.deleteTodo);
const handleAddTodo = async (e: React.FormEvent) => {
e.preventDefault();
const text = newTodoText.trim();
if (text) {
setNewTodoText("");
try {
await createTodo({ text });
} catch (error) {
console.error("Failed to add todo:", error);
setNewTodoText(text);
}
}
};
const handleToggleTodo = async (id: Id<"todos">, completed: boolean) => {
try {
await toggleTodo({ id, completed: !completed });
} catch (error) {
console.error("Failed to toggle todo:", error);
}
};
const handleDeleteTodo = async (id: Id<"todos">) => {
try {
await removeTodo({ id });
} catch (error) {
console.error("Failed to delete todo:", error);
}
};
{{else}}
{{#if (eq api "trpc")}}
const trpc = useTRPC();
{{/if}}
{{#if (eq api "orpc")}}
const orpc = useORPC();
{{/if}}
{{#if (eq api "trpc")}}
const todos = useQuery(trpc.todo.getAll.queryOptions());
const createMutation = useMutation(
trpc.todo.create.mutationOptions({
@@ -53,8 +100,8 @@ function TodosRoute() {
onSuccess: () => todos.refetch(),
}),
);
{{/if}}
{{#if (eq api "orpc")}}
{{/if}}
{{#if (eq api "orpc")}}
const todos = useQuery(orpc.todo.getAll.queryOptions());
const createMutation = useMutation(
orpc.todo.create.mutationOptions({
@@ -74,7 +121,7 @@ function TodosRoute() {
onSuccess: () => todos.refetch(),
}),
);
{{/if}}
{{/if}}
const handleAddTodo = (e: React.FormEvent) => {
e.preventDefault();
@@ -90,12 +137,13 @@ function TodosRoute() {
const handleDeleteTodo = (id: number) => {
deleteMutation.mutate({ id });
};
{{/if}}
return (
<div className="mx-auto w-full max-w-md py-10">
<Card>
<CardHeader>
<CardTitle>Todo List</CardTitle>
<CardTitle>Todo List{{#if (eq backend "convex")}} (Convex){{/if}}</CardTitle>
<CardDescription>Manage your tasks efficiently</CardDescription>
</CardHeader>
<CardContent>
@@ -107,20 +155,72 @@ function TodosRoute() {
value={newTodoText}
onChange={(e) => setNewTodoText(e.target.value)}
placeholder="Add a new task..."
{{#unless (eq backend "convex")}}
disabled={createMutation.isPending}
{{/unless}}
/>
<Button
type="submit"
{{#unless (eq backend "convex")}}
disabled={createMutation.isPending || !newTodoText.trim()}
{{else}}
disabled={!newTodoText.trim()}
{{/unless}}
>
{{#unless (eq backend "convex")}}
{createMutation.isPending ? (
<Loader2 className="h-4 w-4 animate-spin" />
) : (
"Add"
)}
{{else}}
Add
{{/unless}}
</Button>
</form>
{{#if (eq backend "convex")}}
{todos?.length === 0 ? (
<p className="py-4 text-center">No todos yet. Add one above!</p>
) : (
<ul className="space-y-2">
{todos?.map((todo) => (
<li
key={todo._id}
className="flex items-center justify-between rounded-md border p-2"
>
<div className="flex items-center space-x-2">
<Checkbox
checked={todo.completed}
onCheckedChange={() =>
handleToggleTodo(todo._id, todo.completed)
}
id={`todo-${todo._id}`}
/>
<label
htmlFor={`todo-${todo._id}`}
className={`${
todo.completed
? "text-muted-foreground line-through"
: ""
}`}
>
{todo.text}
</label>
</div>
<Button
variant="ghost"
size="icon"
onClick={() => handleDeleteTodo(todo._id)}
aria-label="Delete todo"
>
<Trash2 className="h-4 w-4" />
</Button>
</li>
))}
</ul>
)}
{{else}}
{todos.isLoading ? (
<div className="flex justify-center py-4">
<Loader2 className="h-6 w-6 animate-spin" />
@@ -161,6 +261,7 @@ function TodosRoute() {
))}
</ul>
)}
{{/if}}
</CardContent>
</Card>
</div>