@lark-apaas/coding-templates 0.1.19 → 0.1.23
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/template-apex/README.md +65 -38
- package/template-apex/client/src/api/index.ts +16 -38
- package/template-apex/client/src/app.tsx +2 -4
- package/template-apex/client/src/components/layout.tsx +2 -4
- package/template-apex/client/src/pages/HomePage/HomePage.tsx +12 -0
- package/template-apex/package.json +1 -1
- package/template-apex/server/db/schema.ts +11 -8
- package/template-apex/server/routes/index.ts +5 -2
- package/template-apex/shared/api.interface.ts +11 -21
- package/template-apex/shared/types.ts +8 -7
- package/template-apex/tsconfig.server.json +2 -0
- package/template-apex/client/src/pages/home/index.tsx +0 -20
- package/template-apex/client/src/pages/todos/components/todo-form.tsx +0 -40
- package/template-apex/client/src/pages/todos/components/todo-list.tsx +0 -43
- package/template-apex/client/src/pages/todos/index.tsx +0 -47
- package/template-apex/server/routes/todos.ts +0 -53
- /package/template-apex/client/src/pages/{not-found/index.tsx → NotFoundPage/NotFoundPage.tsx} +0 -0
package/package.json
CHANGED
package/template-apex/README.md
CHANGED
|
@@ -23,20 +23,20 @@
|
|
|
23
23
|
│ ├── index.css # 全局样式 + 主题变量
|
|
24
24
|
│ ├── api/ # API 请求封装
|
|
25
25
|
│ │ └── index.ts
|
|
26
|
-
│ ├── components/ #
|
|
26
|
+
│ ├── components/ # 基础 UI 组件(禁止存放业务组件)
|
|
27
27
|
│ │ └── ui/ # shadcn/ui 内置组件(勿手动修改)
|
|
28
28
|
│ ├── pages/ # 页面模块(每个页面一个目录)
|
|
29
|
-
│ │ ├──
|
|
30
|
-
│ │ └──
|
|
31
|
-
│ │
|
|
32
|
-
│ │
|
|
29
|
+
│ │ ├── HomePage/ # 占位示例页,开发时替换为业务首页
|
|
30
|
+
│ │ │ └── HomePage.tsx # 页面入口文件与目录同名
|
|
31
|
+
│ │ │ └── components/ # 页面专属组件
|
|
32
|
+
│ │ └── NotFoundPage/
|
|
33
|
+
│ │ └── NotFoundPage.tsx
|
|
33
34
|
│ ├── hooks/ # 自定义 Hooks
|
|
34
35
|
│ └── lib/ # 工具函数(cn() 等)
|
|
35
36
|
├── server/ # 后端代码
|
|
36
37
|
│ ├── index.ts # Express 入口(dev: Vite HMR 中间件)
|
|
37
38
|
│ ├── routes/ # API 路由
|
|
38
|
-
│ │
|
|
39
|
-
│ │ └── todos.ts # Todos CRUD 路由
|
|
39
|
+
│ │ └── index.ts # 路由注册
|
|
40
40
|
│ └── db/ # 数据库层
|
|
41
41
|
│ ├── schema.ts # Drizzle schema 定义(可由工具生成)
|
|
42
42
|
│ └── index.ts # 数据库连接
|
|
@@ -47,16 +47,6 @@
|
|
|
47
47
|
|
|
48
48
|
---
|
|
49
49
|
|
|
50
|
-
## 快速开始
|
|
51
|
-
|
|
52
|
-
```bash
|
|
53
|
-
npm install
|
|
54
|
-
cp _env.local.example .env.local # 配置 DATABASE_URL
|
|
55
|
-
npm run dev # 启动开发服务器
|
|
56
|
-
```
|
|
57
|
-
|
|
58
|
-
---
|
|
59
|
-
|
|
60
50
|
## 新增资源(以 posts 为例)
|
|
61
51
|
|
|
62
52
|
### 1. shared/ — 定义类型和校验
|
|
@@ -103,7 +93,7 @@ export const posts = pgTable("posts", {
|
|
|
103
93
|
import { Router } from "express";
|
|
104
94
|
import { db } from "../db/index";
|
|
105
95
|
import { posts } from "../db/schema";
|
|
106
|
-
import { createPostSchema } from "
|
|
96
|
+
import { createPostSchema } from "@shared/api.interface";
|
|
107
97
|
|
|
108
98
|
const router = Router();
|
|
109
99
|
router.get("/", async (_req, res) => {
|
|
@@ -128,20 +118,29 @@ app.use("/api/posts", postsRouter);
|
|
|
128
118
|
|
|
129
119
|
### 3. client/ — API 封装和页面
|
|
130
120
|
|
|
121
|
+
> ⚠️ **客户端所有 HTTP 请求必须使用 `axiosForBackend`**
|
|
122
|
+
>
|
|
123
|
+
> `axiosForBackend` 由 `@lark-apaas/client-toolkit-lite` 提供,内置平台鉴权和请求上下文。**禁止使用** `fetch`、`axios`、`XMLHttpRequest` 或其他 HTTP 客户端直接发起请求。
|
|
124
|
+
|
|
131
125
|
`client/src/api/index.ts` 增加封装:
|
|
132
126
|
|
|
133
127
|
```typescript
|
|
134
|
-
import type {
|
|
135
|
-
|
|
136
|
-
export
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
};
|
|
128
|
+
import type { ListPostsResponse } from "@shared/api.interface";
|
|
129
|
+
|
|
130
|
+
export async function listPosts(): Promise<ListPostsResponse> {
|
|
131
|
+
try {
|
|
132
|
+
const response = await axiosForBackend({
|
|
133
|
+
url: '/api/posts',
|
|
134
|
+
method: 'GET',
|
|
135
|
+
});
|
|
136
|
+
return response.data;
|
|
137
|
+
} catch (error) {
|
|
138
|
+
throw error;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
142
141
|
```
|
|
143
142
|
|
|
144
|
-
`client/src/pages/
|
|
143
|
+
`client/src/pages/PostsPage/PostsPage.tsx` 编写页面,`client/src/app.tsx` 注册路由。
|
|
145
144
|
|
|
146
145
|
---
|
|
147
146
|
|
|
@@ -150,7 +149,7 @@ export const postsApi = {
|
|
|
150
149
|
**页面文件只做骨架编排,不包含具体 UI 实现。**
|
|
151
150
|
|
|
152
151
|
```tsx
|
|
153
|
-
// client/src/pages/
|
|
152
|
+
// client/src/pages/DashboardPage/DashboardPage.tsx
|
|
154
153
|
import { StatsSection } from "./components/stats-section";
|
|
155
154
|
import { DataTableSection } from "./components/data-table-section";
|
|
156
155
|
|
|
@@ -169,29 +168,57 @@ export default function DashboardPage() {
|
|
|
169
168
|
- 每个视觉上独立的区块拆为一个组件文件,即使只出现一次
|
|
170
169
|
- 单个组件文件不超过 **150 行**,超出时进一步拆分子组件
|
|
171
170
|
- 页面专属组件放在 `pages/<page>/components/`
|
|
172
|
-
-
|
|
173
|
-
- 相同 UI 片段出现 **≥2 次**时,必须提取为可复用组件
|
|
171
|
+
- `client/src/components/` 仅存放基础 UI 组件(如 shadcn/ui),**禁止存放业务组件**
|
|
174
172
|
- 文件名 kebab-case(`stat-card.tsx`),组件名 PascalCase(`StatCard`)
|
|
175
173
|
- 组件之间**禁止循环引用**
|
|
176
174
|
|
|
175
|
+
### Section 独立性(并行开发规范)
|
|
176
|
+
|
|
177
|
+
每个 Section 级组件(如 `StatsSection`、`DataTableSection`)必须做到**完全自包含**:
|
|
178
|
+
|
|
179
|
+
**1. 页面文件 = 纯布局组合器**
|
|
180
|
+
|
|
181
|
+
- 页面入口文件(如 `DashboardPage.tsx`)只做 import + JSX 排列,**禁止包含** `useState`、`useEffect`、数据请求或业务逻辑
|
|
182
|
+
- 页面文件无 props 接口定义,不承担任何数据协调职责
|
|
183
|
+
|
|
184
|
+
**2. Section 自包含原则**
|
|
185
|
+
|
|
186
|
+
每个 Section 组件独立拥有自己的:
|
|
187
|
+
- **数据获取**(API 调用、fetch)— 即使多个 Section 需要同一份数据,各自获取
|
|
188
|
+
- **状态管理**(useState、useReducer)
|
|
189
|
+
- **类型定义**(写在同文件或同目录下的 `types.ts`)
|
|
190
|
+
- **子组件**(如需拆分,平铺在 `components/` 目录下,禁止嵌套子目录)
|
|
191
|
+
|
|
192
|
+
**3. 禁止 Section 间横向依赖**
|
|
193
|
+
|
|
194
|
+
- 兄弟 Section 之间**禁止互相 import**
|
|
195
|
+
- 兄弟 Section 之间**禁止通过 Context、全局 store、事件总线、页面 props 共享状态**
|
|
196
|
+
- 每个 Section 可独立开发、独立测试,不依赖其他 Section 的存在
|
|
197
|
+
|
|
177
198
|
---
|
|
178
199
|
|
|
179
200
|
## 路由注册
|
|
180
201
|
|
|
181
|
-
新增页面在 `client/src/app.tsx`
|
|
202
|
+
新增页面在 `client/src/app.tsx` 中注册。
|
|
203
|
+
|
|
204
|
+
> ⚠️ **首页路由替换(必做)**
|
|
205
|
+
>
|
|
206
|
+
> 模板默认的 `HomePage` 是占位示例页,**不是业务首页**。开发时必须将 `index` 路由替换为真实的业务首页组件(如 `DashboardPage`),并删除 `HomePage` 目录。
|
|
207
|
+
|
|
208
|
+
**替换后的路由示例:**
|
|
182
209
|
|
|
183
210
|
```tsx
|
|
184
211
|
<Route element={<Layout />}>
|
|
185
|
-
|
|
212
|
+
{/* 将 index 路由指向真实的业务首页 */}
|
|
213
|
+
<Route index element={<DashboardPage />} />
|
|
186
214
|
<Route path="todos" element={<TodosPage />} />
|
|
187
|
-
<Route path="dashboard" element={<DashboardPage />} />
|
|
188
215
|
<Route path="*" element={<NotFoundPage />} />
|
|
189
216
|
</Route>
|
|
190
217
|
```
|
|
191
218
|
|
|
192
219
|
**新增页面步骤:**
|
|
193
220
|
|
|
194
|
-
1. 在 `client/src/pages/`
|
|
221
|
+
1. 在 `client/src/pages/` 下新建页面目录(如 `SettingsPage`)和 `SettingsPage.tsx`
|
|
195
222
|
2. 在 `app.tsx` 的 `<Routes>` 内添加 `<Route>` 配置
|
|
196
223
|
|
|
197
224
|
**路由跳转必须使用 react-router-dom:**
|
|
@@ -281,10 +308,10 @@ export default function DashboardPage() {
|
|
|
281
308
|
|
|
282
309
|
| 检查项 | 验收标准 |
|
|
283
310
|
|--------|---------|
|
|
284
|
-
| 页面拆分 |
|
|
285
|
-
|
|
|
286
|
-
| 路由注册 |
|
|
287
|
-
| API 调用 | 统一在 `api/`
|
|
311
|
+
| 页面拆分 | 页面文件只做骨架编排(无 state/effect/逻辑);每个 Section 自包含数据+状态+类型;兄弟 Section 间无互相 import;单文件 ≤150 行 |
|
|
312
|
+
| 命名规范 | 页面目录 PascalCase,页面入口文件与目录同名(PascalCase),组件文件名 kebab-case,组件名 PascalCase |
|
|
313
|
+
| 路由注册 | 默认 `HomePage` 已替换为业务首页;新页面已在 `app.tsx` 注册;跳转使用 `<Link>` / `useNavigate()`,无 `<a href>` |
|
|
314
|
+
| API 调用 | 统一在 `api/` 封装;必须使用 `axiosForBackend` 发起请求,禁止 `fetch`/`axios`;使用 `@shared` 类型 |
|
|
288
315
|
| 输入校验 | zod schema 定义在 `shared/api.interface.ts`;server 和 client 共用 |
|
|
289
316
|
| 主题色 | 使用语义化变量类(`bg-background`、`text-primary` 等);未硬编码颜色值 |
|
|
290
317
|
| 主题修改 | 仅增量覆盖变更的变量;新增色同时注册 `:root` 和 `@theme inline` |
|
|
@@ -1,39 +1,17 @@
|
|
|
1
|
-
import
|
|
2
|
-
|
|
3
|
-
CreateTodoResponse,
|
|
4
|
-
UpdateTodoRequest,
|
|
5
|
-
UpdateTodoResponse,
|
|
6
|
-
DeleteTodoResponse,
|
|
7
|
-
ListTodosResponse,
|
|
8
|
-
} from "@shared/api.interface";
|
|
1
|
+
import { axiosForBackend } from '@lark-apaas/client-toolkit-lite/utils/getAxiosForBackend';
|
|
2
|
+
// import type { ListPostsResponse } from '@shared/api.interface';
|
|
9
3
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
create: (data: CreateTodoRequest) =>
|
|
26
|
-
request<CreateTodoResponse>("/api/todos", {
|
|
27
|
-
method: "POST",
|
|
28
|
-
body: JSON.stringify(data),
|
|
29
|
-
}),
|
|
30
|
-
|
|
31
|
-
update: (id: number, data: UpdateTodoRequest) =>
|
|
32
|
-
request<UpdateTodoResponse>(`/api/todos/${id}`, {
|
|
33
|
-
method: "PATCH",
|
|
34
|
-
body: JSON.stringify(data),
|
|
35
|
-
}),
|
|
36
|
-
|
|
37
|
-
remove: (id: number) =>
|
|
38
|
-
request<DeleteTodoResponse>(`/api/todos/${id}`, { method: "DELETE" }),
|
|
39
|
-
};
|
|
4
|
+
// Add more API functions here, use axios instance (`axiosForBackend`) to make requests.
|
|
5
|
+
//
|
|
6
|
+
// 使用示例:
|
|
7
|
+
// export async function listPosts(): Promise<ListPostsResponse> {
|
|
8
|
+
// try {
|
|
9
|
+
// const response = await axiosForBackend({
|
|
10
|
+
// url: '/api/posts',
|
|
11
|
+
// method: 'GET',
|
|
12
|
+
// });
|
|
13
|
+
// return response.data;
|
|
14
|
+
// } catch (error) {
|
|
15
|
+
// throw error;
|
|
16
|
+
// }
|
|
17
|
+
// }
|
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
import { BrowserRouter, Routes, Route } from "react-router-dom";
|
|
2
2
|
import { Layout } from "@/components/layout";
|
|
3
|
-
import HomePage from "@/pages/
|
|
4
|
-
import
|
|
5
|
-
import NotFoundPage from "@/pages/not-found";
|
|
3
|
+
import HomePage from "@/pages/HomePage/HomePage";
|
|
4
|
+
import NotFoundPage from "@/pages/NotFoundPage/NotFoundPage";
|
|
6
5
|
|
|
7
6
|
export default function App() {
|
|
8
7
|
return (
|
|
@@ -10,7 +9,6 @@ export default function App() {
|
|
|
10
9
|
<Routes>
|
|
11
10
|
<Route element={<Layout />}>
|
|
12
11
|
<Route index element={<HomePage />} />
|
|
13
|
-
<Route path="todos" element={<TodosPage />} />
|
|
14
12
|
<Route path="*" element={<NotFoundPage />} />
|
|
15
13
|
</Route>
|
|
16
14
|
</Routes>
|
|
@@ -2,10 +2,8 @@ import { Outlet } from "react-router-dom";
|
|
|
2
2
|
|
|
3
3
|
export function Layout() {
|
|
4
4
|
return (
|
|
5
|
-
<div className="
|
|
6
|
-
<
|
|
7
|
-
<Outlet />
|
|
8
|
-
</main>
|
|
5
|
+
<div className="w-screen h-screen">
|
|
6
|
+
<Outlet />
|
|
9
7
|
</div>
|
|
10
8
|
);
|
|
11
9
|
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export default function HomePage() {
|
|
2
|
+
return (
|
|
3
|
+
<div className="flex flex-col items-center justify-center py-24">
|
|
4
|
+
<h1 className="text-5xl font-bold tracking-tight sm:text-7xl mb-4">
|
|
5
|
+
👋 Hello OpenClaw
|
|
6
|
+
</h1>
|
|
7
|
+
<p className="text-lg text-muted-foreground">
|
|
8
|
+
Start building your app by editing <code className="px-1.5 py-0.5 rounded bg-muted font-mono text-sm">client/src/pages/HomePage/HomePage.tsx</code>
|
|
9
|
+
</p>
|
|
10
|
+
</div>
|
|
11
|
+
);
|
|
12
|
+
}
|
|
@@ -1,8 +1,11 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
1
|
+
// Drizzle schema 定义(可由工具生成)
|
|
2
|
+
//
|
|
3
|
+
// import { pgTable, serial, text, timestamp } from "drizzle-orm/pg-core";
|
|
4
|
+
//
|
|
5
|
+
// 使用示例:
|
|
6
|
+
// export const posts = pgTable("posts", {
|
|
7
|
+
// id: serial("id").primaryKey(),
|
|
8
|
+
// title: text("title").notNull(),
|
|
9
|
+
// content: text("content"),
|
|
10
|
+
// createdAt: timestamp("created_at").notNull().defaultNow(),
|
|
11
|
+
// });
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import type { Express } from "express";
|
|
2
|
-
import
|
|
2
|
+
// import postsRouter from "./posts";
|
|
3
3
|
|
|
4
4
|
export function registerRoutes(app: Express) {
|
|
5
|
-
|
|
5
|
+
// 在此注册 API 路由
|
|
6
|
+
//
|
|
7
|
+
// 使用示例:
|
|
8
|
+
// app.use("/api/posts", postsRouter);
|
|
6
9
|
}
|
|
@@ -1,23 +1,13 @@
|
|
|
1
1
|
// API 接口契约定义
|
|
2
2
|
// zod schema 做校验,z.infer 推导类型,前后端共享
|
|
3
|
-
|
|
4
|
-
import { z } from "zod";
|
|
5
|
-
import type {
|
|
6
|
-
|
|
7
|
-
//
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
});
|
|
12
|
-
|
|
13
|
-
export
|
|
14
|
-
title: z.string().min(1).max(500).optional(),
|
|
15
|
-
completed: z.boolean().optional(),
|
|
16
|
-
});
|
|
17
|
-
|
|
18
|
-
export type CreateTodoRequest = z.infer<typeof createTodoSchema>;
|
|
19
|
-
export type UpdateTodoRequest = z.infer<typeof updateTodoSchema>;
|
|
20
|
-
export type CreateTodoResponse = Todo;
|
|
21
|
-
export type UpdateTodoResponse = Todo;
|
|
22
|
-
export type DeleteTodoResponse = Todo;
|
|
23
|
-
export type ListTodosResponse = Todo[];
|
|
3
|
+
//
|
|
4
|
+
// import { z } from "zod";
|
|
5
|
+
// import type { Post } from "./types";
|
|
6
|
+
//
|
|
7
|
+
// 使用示例:
|
|
8
|
+
// export const createPostSchema = z.object({
|
|
9
|
+
// title: z.string().min(1).max(200),
|
|
10
|
+
// content: z.string().optional(),
|
|
11
|
+
// });
|
|
12
|
+
// export type CreatePostRequest = z.infer<typeof createPostSchema>;
|
|
13
|
+
// export type CreatePostResponse = Post;
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
// 前后端共享类型定义
|
|
2
2
|
// shared 是最底层模块,不依赖 client 或 server
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
3
|
+
//
|
|
4
|
+
// 使用示例:
|
|
5
|
+
// export interface Post {
|
|
6
|
+
// id: number;
|
|
7
|
+
// title: string;
|
|
8
|
+
// content: string | null;
|
|
9
|
+
// createdAt: string;
|
|
10
|
+
// }
|
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
import { Link } from "react-router-dom";
|
|
2
|
-
|
|
3
|
-
export default function HomePage() {
|
|
4
|
-
return (
|
|
5
|
-
<div className="flex flex-col items-center justify-center py-24">
|
|
6
|
-
<h1 className="text-5xl font-bold tracking-tight sm:text-7xl mb-4">
|
|
7
|
-
Hello OpenClaw
|
|
8
|
-
</h1>
|
|
9
|
-
<p className="text-lg text-muted-foreground mb-8">
|
|
10
|
-
Apex full-stack template
|
|
11
|
-
</p>
|
|
12
|
-
<Link
|
|
13
|
-
to="/todos"
|
|
14
|
-
className="inline-flex items-center justify-center rounded-md bg-primary px-6 py-3 text-sm font-medium text-primary-foreground hover:bg-primary/90"
|
|
15
|
-
>
|
|
16
|
-
Try the Todos Demo
|
|
17
|
-
</Link>
|
|
18
|
-
</div>
|
|
19
|
-
);
|
|
20
|
-
}
|
|
@@ -1,40 +0,0 @@
|
|
|
1
|
-
import { useState } from "react";
|
|
2
|
-
|
|
3
|
-
interface TodoFormProps {
|
|
4
|
-
onSubmit: (title: string) => Promise<void>;
|
|
5
|
-
}
|
|
6
|
-
|
|
7
|
-
export function TodoForm({ onSubmit }: TodoFormProps) {
|
|
8
|
-
const [title, setTitle] = useState("");
|
|
9
|
-
const [submitting, setSubmitting] = useState(false);
|
|
10
|
-
|
|
11
|
-
const handleSubmit = async (e: React.FormEvent) => {
|
|
12
|
-
e.preventDefault();
|
|
13
|
-
const trimmed = title.trim();
|
|
14
|
-
if (!trimmed) return;
|
|
15
|
-
setSubmitting(true);
|
|
16
|
-
await onSubmit(trimmed);
|
|
17
|
-
setTitle("");
|
|
18
|
-
setSubmitting(false);
|
|
19
|
-
};
|
|
20
|
-
|
|
21
|
-
return (
|
|
22
|
-
<form onSubmit={handleSubmit} className="flex gap-2">
|
|
23
|
-
<input
|
|
24
|
-
type="text"
|
|
25
|
-
value={title}
|
|
26
|
-
onChange={(e) => setTitle(e.target.value)}
|
|
27
|
-
placeholder="What needs to be done?"
|
|
28
|
-
className="flex-1 rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
|
29
|
-
disabled={submitting}
|
|
30
|
-
/>
|
|
31
|
-
<button
|
|
32
|
-
type="submit"
|
|
33
|
-
disabled={submitting || !title.trim()}
|
|
34
|
-
className="inline-flex items-center justify-center rounded-md bg-primary px-4 py-2 text-sm font-medium text-primary-foreground hover:bg-primary/90 disabled:pointer-events-none disabled:opacity-50"
|
|
35
|
-
>
|
|
36
|
-
Add
|
|
37
|
-
</button>
|
|
38
|
-
</form>
|
|
39
|
-
);
|
|
40
|
-
}
|
|
@@ -1,43 +0,0 @@
|
|
|
1
|
-
import type { Todo } from "@shared/types";
|
|
2
|
-
import { Trash2 } from "lucide-react";
|
|
3
|
-
|
|
4
|
-
interface TodoListProps {
|
|
5
|
-
todos: Todo[];
|
|
6
|
-
onToggle: (id: number, completed: boolean) => Promise<void>;
|
|
7
|
-
onDelete: (id: number) => Promise<void>;
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
export function TodoList({ todos, onToggle, onDelete }: TodoListProps) {
|
|
11
|
-
if (todos.length === 0) {
|
|
12
|
-
return <p className="text-muted-foreground mt-8 text-center">No todos yet.</p>;
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
return (
|
|
16
|
-
<ul className="mt-6 space-y-2">
|
|
17
|
-
{todos.map((todo) => (
|
|
18
|
-
<li
|
|
19
|
-
key={todo.id}
|
|
20
|
-
className="flex items-center gap-3 rounded-md border border-border px-4 py-3"
|
|
21
|
-
>
|
|
22
|
-
<input
|
|
23
|
-
type="checkbox"
|
|
24
|
-
checked={todo.completed}
|
|
25
|
-
onChange={() => onToggle(todo.id, !todo.completed)}
|
|
26
|
-
className="h-4 w-4 rounded border-border"
|
|
27
|
-
/>
|
|
28
|
-
<span
|
|
29
|
-
className={`flex-1 text-sm ${todo.completed ? "line-through text-muted-foreground" : ""}`}
|
|
30
|
-
>
|
|
31
|
-
{todo.title}
|
|
32
|
-
</span>
|
|
33
|
-
<button
|
|
34
|
-
onClick={() => onDelete(todo.id)}
|
|
35
|
-
className="text-muted-foreground hover:text-destructive transition-colors"
|
|
36
|
-
>
|
|
37
|
-
<Trash2 className="h-4 w-4" />
|
|
38
|
-
</button>
|
|
39
|
-
</li>
|
|
40
|
-
))}
|
|
41
|
-
</ul>
|
|
42
|
-
);
|
|
43
|
-
}
|
|
@@ -1,47 +0,0 @@
|
|
|
1
|
-
import { useEffect, useState } from "react";
|
|
2
|
-
import type { Todo } from "@shared/types";
|
|
3
|
-
import { todosApi } from "@/api";
|
|
4
|
-
import { TodoForm } from "./components/todo-form";
|
|
5
|
-
import { TodoList } from "./components/todo-list";
|
|
6
|
-
|
|
7
|
-
export default function TodosPage() {
|
|
8
|
-
const [todos, setTodos] = useState<Todo[]>([]);
|
|
9
|
-
const [loading, setLoading] = useState(true);
|
|
10
|
-
|
|
11
|
-
const refresh = async () => {
|
|
12
|
-
const list = await todosApi.list();
|
|
13
|
-
setTodos(list);
|
|
14
|
-
setLoading(false);
|
|
15
|
-
};
|
|
16
|
-
|
|
17
|
-
useEffect(() => {
|
|
18
|
-
refresh();
|
|
19
|
-
}, []);
|
|
20
|
-
|
|
21
|
-
const handleCreate = async (title: string) => {
|
|
22
|
-
await todosApi.create({ title });
|
|
23
|
-
await refresh();
|
|
24
|
-
};
|
|
25
|
-
|
|
26
|
-
const handleToggle = async (id: number, completed: boolean) => {
|
|
27
|
-
await todosApi.update(id, { completed });
|
|
28
|
-
await refresh();
|
|
29
|
-
};
|
|
30
|
-
|
|
31
|
-
const handleDelete = async (id: number) => {
|
|
32
|
-
await todosApi.remove(id);
|
|
33
|
-
await refresh();
|
|
34
|
-
};
|
|
35
|
-
|
|
36
|
-
return (
|
|
37
|
-
<div className="max-w-2xl mx-auto py-12">
|
|
38
|
-
<h1 className="text-3xl font-bold mb-8">Todos</h1>
|
|
39
|
-
<TodoForm onSubmit={handleCreate} />
|
|
40
|
-
{loading ? (
|
|
41
|
-
<p className="text-muted-foreground mt-8">Loading...</p>
|
|
42
|
-
) : (
|
|
43
|
-
<TodoList todos={todos} onToggle={handleToggle} onDelete={handleDelete} />
|
|
44
|
-
)}
|
|
45
|
-
</div>
|
|
46
|
-
);
|
|
47
|
-
}
|
|
@@ -1,53 +0,0 @@
|
|
|
1
|
-
import { Router } from "express";
|
|
2
|
-
import { eq, desc } from "drizzle-orm";
|
|
3
|
-
import { db } from "../db/index";
|
|
4
|
-
import { todos } from "../db/schema";
|
|
5
|
-
import { createTodoSchema, updateTodoSchema } from "../../shared/api.interface";
|
|
6
|
-
|
|
7
|
-
const router = Router();
|
|
8
|
-
|
|
9
|
-
// GET /api/todos
|
|
10
|
-
router.get("/", async (_req, res) => {
|
|
11
|
-
const list = await db.select().from(todos).orderBy(desc(todos.createdAt));
|
|
12
|
-
res.json(list);
|
|
13
|
-
});
|
|
14
|
-
|
|
15
|
-
// POST /api/todos
|
|
16
|
-
router.post("/", async (req, res) => {
|
|
17
|
-
const parsed = createTodoSchema.safeParse(req.body);
|
|
18
|
-
if (!parsed.success) {
|
|
19
|
-
res.status(400).json({ error: parsed.error.flatten() });
|
|
20
|
-
return;
|
|
21
|
-
}
|
|
22
|
-
const [todo] = await db.insert(todos).values({ title: parsed.data.title }).returning();
|
|
23
|
-
res.status(201).json(todo);
|
|
24
|
-
});
|
|
25
|
-
|
|
26
|
-
// PATCH /api/todos/:id
|
|
27
|
-
router.patch("/:id", async (req, res) => {
|
|
28
|
-
const id = Number(req.params.id);
|
|
29
|
-
const parsed = updateTodoSchema.safeParse(req.body);
|
|
30
|
-
if (!parsed.success) {
|
|
31
|
-
res.status(400).json({ error: parsed.error.flatten() });
|
|
32
|
-
return;
|
|
33
|
-
}
|
|
34
|
-
const [todo] = await db.update(todos).set(parsed.data).where(eq(todos.id, id)).returning();
|
|
35
|
-
if (!todo) {
|
|
36
|
-
res.status(404).json({ error: "Not found" });
|
|
37
|
-
return;
|
|
38
|
-
}
|
|
39
|
-
res.json(todo);
|
|
40
|
-
});
|
|
41
|
-
|
|
42
|
-
// DELETE /api/todos/:id
|
|
43
|
-
router.delete("/:id", async (req, res) => {
|
|
44
|
-
const id = Number(req.params.id);
|
|
45
|
-
const [todo] = await db.delete(todos).where(eq(todos.id, id)).returning();
|
|
46
|
-
if (!todo) {
|
|
47
|
-
res.status(404).json({ error: "Not found" });
|
|
48
|
-
return;
|
|
49
|
-
}
|
|
50
|
-
res.json(todo);
|
|
51
|
-
});
|
|
52
|
-
|
|
53
|
-
export default router;
|
/package/template-apex/client/src/pages/{not-found/index.tsx → NotFoundPage/NotFoundPage.tsx}
RENAMED
|
File without changes
|