diff --git a/drizzle/20260401114858_cultured_lady_mastermind/snapshot.json b/drizzle/20260401114858_cultured_lady_mastermind/snapshot.json index e8663ab..fb368f0 100644 --- a/drizzle/20260401114858_cultured_lady_mastermind/snapshot.json +++ b/drizzle/20260401114858_cultured_lady_mastermind/snapshot.json @@ -2,9 +2,7 @@ "version": "8", "dialect": "postgres", "id": "91ea16cc-3353-493c-bc2c-4fc9dea2fce7", - "prevIds": [ - "00000000-0000-0000-0000-000000000000" - ], + "prevIds": ["00000000-0000-0000-0000-000000000000"], "ddl": [ { "isRlsEnabled": false, @@ -78,9 +76,7 @@ "table": "todo" }, { - "columns": [ - "id" - ], + "columns": ["id"], "nameExplicit": false, "name": "todo_pkey", "schema": "public", @@ -89,4 +85,4 @@ } ], "renames": [] -} \ No newline at end of file +} diff --git a/src/components/Error.tsx b/src/components/Error.tsx index f25b194..193d098 100644 --- a/src/components/Error.tsx +++ b/src/components/Error.tsx @@ -1,3 +1,40 @@ -export function ErrorComponent() { - return
An unhandled error happened!
+export const ErrorComponent = ({ error, reset }: { error: Error; reset: () => void }) => { + return ( +
+
+
+ +
+
+

出错了

+

{error.message}

+
+
+ + + 返回首页 + +
+
+
+ ) } diff --git a/src/components/NotFound.tsx b/src/components/NotFound.tsx index fec55b7..d448827 100644 --- a/src/components/NotFound.tsx +++ b/src/components/NotFound.tsx @@ -1,3 +1,23 @@ -export function NotFoundComponent() { - return
404 - Not Found
+import { Link } from '@tanstack/react-router' + +export const NotFoundComponent = () => { + return ( +
+
+
+ 404 +
+
+

页面不存在

+

您访问的页面可能已被移除或地址有误

+
+ + 返回首页 + +
+
+ ) } diff --git a/src/components/TodoForm.tsx b/src/components/TodoForm.tsx new file mode 100644 index 0000000..3f92f6b --- /dev/null +++ b/src/components/TodoForm.tsx @@ -0,0 +1,41 @@ +import type { SubmitEventHandler } from 'react' +import { useState } from 'react' + +interface TodoFormProps { + onSubmit: (title: string) => void + isPending: boolean +} + +export const TodoForm = ({ onSubmit, isPending }: TodoFormProps) => { + const [title, setTitle] = useState('') + + const handleSubmit: SubmitEventHandler = (e) => { + e.preventDefault() + if (title.trim()) { + onSubmit(title.trim()) + setTitle('') + } + } + + return ( +
+
+ setTitle(e.target.value)} + placeholder="添加新任务..." + className="w-full pl-6 pr-32 py-5 bg-white rounded-2xl shadow-[0_8px_30px_rgb(0,0,0,0.04)] border-0 ring-1 ring-slate-100 focus:ring-2 focus:ring-indigo-500/50 outline-none transition-all placeholder:text-slate-400 text-lg text-slate-700" + disabled={isPending} + /> + +
+
+ ) +} diff --git a/src/components/TodoItem.tsx b/src/components/TodoItem.tsx new file mode 100644 index 0000000..1963ff3 --- /dev/null +++ b/src/components/TodoItem.tsx @@ -0,0 +1,77 @@ +import type { RouterOutputs } from '@/server/api/types' + +type Todo = RouterOutputs['todo']['list'][number] + +interface TodoItemProps { + todo: Todo + onToggle: (id: string, completed: boolean) => void + onDelete: (id: string) => void +} + +export const TodoItem = ({ todo, onToggle, onDelete }: TodoItemProps) => { + return ( +
+ + +
+

+ {todo.title} +

+
+ +
+ + {new Date(todo.createdAt).toLocaleDateString('zh-CN')} + + +
+
+ ) +} diff --git a/src/routes/__root.tsx b/src/routes/__root.tsx index b7f758f..5dea9b1 100644 --- a/src/routes/__root.tsx +++ b/src/routes/__root.tsx @@ -34,8 +34,8 @@ export const Route = createRootRouteWithContext()({ ], }), shellComponent: RootDocument, - errorComponent: () => , - notFoundComponent: () => , + errorComponent: ErrorComponent, + notFoundComponent: NotFoundComponent, }) function RootDocument({ children }: Readonly<{ children: ReactNode }>) { diff --git a/src/routes/index.tsx b/src/routes/index.tsx index fa57a89..d3d052e 100644 --- a/src/routes/index.tsx +++ b/src/routes/index.tsx @@ -1,8 +1,8 @@ import { useMutation, useSuspenseQuery } from '@tanstack/react-query' import { createFileRoute } from '@tanstack/react-router' -import type { ChangeEventHandler, SubmitEventHandler } from 'react' -import { useState } from 'react' import { orpc } from '@/client/orpc' +import { TodoForm } from '@/components/TodoForm' +import { TodoItem } from '@/components/TodoItem' export const Route = createFileRoute('/')({ component: Todos, @@ -12,36 +12,11 @@ export const Route = createFileRoute('/')({ }) function Todos() { - const [newTodoTitle, setNewTodoTitle] = useState('') - const listQuery = useSuspenseQuery(orpc.todo.list.queryOptions()) const createMutation = useMutation(orpc.todo.create.mutationOptions()) const updateMutation = useMutation(orpc.todo.update.mutationOptions()) const deleteMutation = useMutation(orpc.todo.remove.mutationOptions()) - const handleCreateTodo: SubmitEventHandler = (e) => { - e.preventDefault() - if (newTodoTitle.trim()) { - createMutation.mutate({ title: newTodoTitle.trim() }) - setNewTodoTitle('') - } - } - - const handleInputChange: ChangeEventHandler = (e) => { - setNewTodoTitle(e.target.value) - } - - const handleToggleTodo = (id: string, currentCompleted: boolean) => { - updateMutation.mutate({ - id, - data: { completed: !currentCompleted }, - }) - } - - const handleDeleteTodo = (id: string) => { - deleteMutation.mutate({ id }) - } - const todos = listQuery.data const completedCount = todos.filter((todo) => todo.completed).length const totalCount = todos.length @@ -65,28 +40,9 @@ function Todos() { - {/* Add Todo Form */} -
-
- - -
-
+ createMutation.mutate({ title })} isPending={createMutation.isPending} /> - {/* Progress Bar (Only visible when there are tasks) */} + {/* Progress Bar */} {totalCount > 0 && (
) : ( todos.map((todo) => ( -
- - -
-

- {todo.title} -

-
- -
- - {new Date(todo.createdAt).toLocaleDateString('zh-CN')} - - -
-
+ todo={todo} + onToggle={(id, completed) => updateMutation.mutate({ id, data: { completed: !completed } })} + onDelete={(id) => deleteMutation.mutate({ id })} + /> )) )}
diff --git a/src/server/api/routers/index.ts b/src/server/api/routers/index.ts index 02a11fe..31a82ed 100644 --- a/src/server/api/routers/index.ts +++ b/src/server/api/routers/index.ts @@ -1,4 +1,4 @@ -import { os } from '../server' +import { os } from '@/server/api/server' import * as todo from './todo.router' export const router = os.router({ diff --git a/src/server/api/routers/todo.router.ts b/src/server/api/routers/todo.router.ts index 40c988f..523e999 100644 --- a/src/server/api/routers/todo.router.ts +++ b/src/server/api/routers/todo.router.ts @@ -1,8 +1,8 @@ import { ORPCError } from '@orpc/server' import { eq } from 'drizzle-orm' +import { db } from '@/server/api/middlewares' +import { os } from '@/server/api/server' import { todoTable } from '@/server/db/schema' -import { db } from '../middlewares' -import { os } from '../server' export const list = os.todo.list.use(db).handler(async ({ context }) => { const todos = await context.db.query.todoTable.findMany({ diff --git a/src/server/db/index.ts b/src/server/db/index.ts index e7fd5f0..fc8e6be 100644 --- a/src/server/db/index.ts +++ b/src/server/db/index.ts @@ -13,11 +13,7 @@ export type DB = ReturnType export const getDB = (() => { let db: DB | null = null - return (singleton = true): DB => { - if (!singleton) { - return createDB() - } - + return (): DB => { db ??= createDB() return db }