@lark-apaas/coding-templates 0.1.12 → 0.1.15

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.
Files changed (98) hide show
  1. package/meta.json +6 -0
  2. package/package.json +2 -1
  3. package/template-apex/README.md +294 -0
  4. package/template-apex/_env.local.example +1 -0
  5. package/template-apex/_gitignore +27 -0
  6. package/template-apex/client/index.html +20 -0
  7. package/template-apex/client/public/favicon.svg +1 -0
  8. package/template-apex/client/public/icons.svg +24 -0
  9. package/template-apex/client/src/api/index.ts +39 -0
  10. package/template-apex/client/src/app.tsx +19 -0
  11. package/template-apex/client/src/components/layout.tsx +11 -0
  12. package/template-apex/client/src/components/ui/accordion.tsx +66 -0
  13. package/template-apex/client/src/components/ui/alert-dialog.tsx +157 -0
  14. package/template-apex/client/src/components/ui/alert.tsx +71 -0
  15. package/template-apex/client/src/components/ui/aspect-ratio.tsx +11 -0
  16. package/template-apex/client/src/components/ui/avatar.tsx +53 -0
  17. package/template-apex/client/src/components/ui/badge.tsx +42 -0
  18. package/template-apex/client/src/components/ui/breadcrumb.tsx +109 -0
  19. package/template-apex/client/src/components/ui/button-group.tsx +83 -0
  20. package/template-apex/client/src/components/ui/button.tsx +69 -0
  21. package/template-apex/client/src/components/ui/calendar.tsx +213 -0
  22. package/template-apex/client/src/components/ui/card.tsx +82 -0
  23. package/template-apex/client/src/components/ui/carousel.tsx +241 -0
  24. package/template-apex/client/src/components/ui/chart.tsx +357 -0
  25. package/template-apex/client/src/components/ui/checkbox.tsx +32 -0
  26. package/template-apex/client/src/components/ui/collapsible.tsx +33 -0
  27. package/template-apex/client/src/components/ui/command.tsx +208 -0
  28. package/template-apex/client/src/components/ui/context-menu.tsx +324 -0
  29. package/template-apex/client/src/components/ui/dialog.tsx +143 -0
  30. package/template-apex/client/src/components/ui/drawer.tsx +135 -0
  31. package/template-apex/client/src/components/ui/dropdown-menu.tsx +329 -0
  32. package/template-apex/client/src/components/ui/empty.tsx +104 -0
  33. package/template-apex/client/src/components/ui/field.tsx +248 -0
  34. package/template-apex/client/src/components/ui/form.tsx +167 -0
  35. package/template-apex/client/src/components/ui/hover-card.tsx +44 -0
  36. package/template-apex/client/src/components/ui/image.tsx +183 -0
  37. package/template-apex/client/src/components/ui/input-group.tsx +166 -0
  38. package/template-apex/client/src/components/ui/input-otp.tsx +77 -0
  39. package/template-apex/client/src/components/ui/input.tsx +21 -0
  40. package/template-apex/client/src/components/ui/item.tsx +193 -0
  41. package/template-apex/client/src/components/ui/kbd.tsx +28 -0
  42. package/template-apex/client/src/components/ui/label.tsx +24 -0
  43. package/template-apex/client/src/components/ui/menubar.tsx +348 -0
  44. package/template-apex/client/src/components/ui/native-select.tsx +48 -0
  45. package/template-apex/client/src/components/ui/navigation-menu.tsx +168 -0
  46. package/template-apex/client/src/components/ui/pagination.tsx +127 -0
  47. package/template-apex/client/src/components/ui/popover.tsx +48 -0
  48. package/template-apex/client/src/components/ui/progress.tsx +31 -0
  49. package/template-apex/client/src/components/ui/radio-group.tsx +45 -0
  50. package/template-apex/client/src/components/ui/resizable.tsx +56 -0
  51. package/template-apex/client/src/components/ui/scroll-area.tsx +58 -0
  52. package/template-apex/client/src/components/ui/select.tsx +243 -0
  53. package/template-apex/client/src/components/ui/separator.tsx +28 -0
  54. package/template-apex/client/src/components/ui/sheet.tsx +139 -0
  55. package/template-apex/client/src/components/ui/sidebar.tsx +727 -0
  56. package/template-apex/client/src/components/ui/skeleton.tsx +13 -0
  57. package/template-apex/client/src/components/ui/slider.tsx +87 -0
  58. package/template-apex/client/src/components/ui/sonner.tsx +67 -0
  59. package/template-apex/client/src/components/ui/spinner.tsx +16 -0
  60. package/template-apex/client/src/components/ui/switch.tsx +31 -0
  61. package/template-apex/client/src/components/ui/table.tsx +116 -0
  62. package/template-apex/client/src/components/ui/tabs.tsx +66 -0
  63. package/template-apex/client/src/components/ui/textarea.tsx +18 -0
  64. package/template-apex/client/src/components/ui/toggle-group.tsx +83 -0
  65. package/template-apex/client/src/components/ui/toggle.tsx +47 -0
  66. package/template-apex/client/src/components/ui/tooltip.tsx +61 -0
  67. package/template-apex/client/src/hooks/use-mobile.ts +19 -0
  68. package/template-apex/client/src/index.css +131 -0
  69. package/template-apex/client/src/lib/utils.ts +6 -0
  70. package/template-apex/client/src/main.tsx +11 -0
  71. package/template-apex/client/src/pages/home/index.tsx +20 -0
  72. package/template-apex/client/src/pages/todos/components/todo-form.tsx +40 -0
  73. package/template-apex/client/src/pages/todos/components/todo-list.tsx +43 -0
  74. package/template-apex/client/src/pages/todos/index.tsx +47 -0
  75. package/template-apex/components.json +21 -0
  76. package/template-apex/eslint.config.js +54 -0
  77. package/template-apex/package.json +91 -0
  78. package/template-apex/scripts/build.sh +56 -0
  79. package/template-apex/server/db/index.ts +8 -0
  80. package/template-apex/server/db/schema.ts +8 -0
  81. package/template-apex/server/index.ts +34 -0
  82. package/template-apex/server/routes/index.ts +6 -0
  83. package/template-apex/server/routes/todos.ts +53 -0
  84. package/template-apex/server/types.d.ts +4 -0
  85. package/template-apex/shared/api.interface.ts +23 -0
  86. package/template-apex/shared/types.ts +9 -0
  87. package/template-apex/tsconfig.app.json +33 -0
  88. package/template-apex/tsconfig.json +14 -0
  89. package/template-apex/tsconfig.node.json +31 -0
  90. package/template-apex/tsconfig.server.json +19 -0
  91. package/template-apex/vite.config.ts +18 -0
  92. package/template-vite-react/README.md +23 -8
  93. package/template-vite-react/client/src/app.tsx +2 -2
  94. package/template-vite-react/client/src/components/layout.tsx +2 -4
  95. package/template-vite-react/client/src/lib/utils.ts +3 -3
  96. package/template-vite-react/client/src/pages/{home/index.tsx → HomePage/HomePage.tsx} +1 -1
  97. package/template-vite-react/client/src/pages/NotFoundPage/NotFoundPage.tsx +11 -0
  98. /package/{template-vite-react → template-apex}/client/src/pages/not-found/index.tsx +0 -0
package/meta.json CHANGED
@@ -17,6 +17,12 @@
17
17
  "dir": "template-nextjs-fullstack",
18
18
  "description": "Next.js App Router + Drizzle + PostgreSQL — 全栈应用",
19
19
  "features": ["typescript", "tailwind", "shadcn-ui", "dark-mode", "ssr", "server-actions", "drizzle", "postgresql"]
20
+ },
21
+ {
22
+ "name": "apex",
23
+ "dir": "template-apex",
24
+ "description": "Vite + React + Express + Drizzle + PostgreSQL — 全栈应用",
25
+ "features": ["typescript", "tailwind", "shadcn-ui", "dark-mode", "express", "drizzle", "postgresql"]
20
26
  }
21
27
  ]
22
28
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lark-apaas/coding-templates",
3
- "version": "0.1.12",
3
+ "version": "0.1.15",
4
4
  "description": "OpenClaw project templates for mclaw CLI",
5
5
  "publishConfig": {
6
6
  "access": "public"
@@ -8,6 +8,7 @@
8
8
  "files": [
9
9
  "template-html",
10
10
  "template-vite-react",
11
+ "template-apex",
11
12
  "meta.json"
12
13
  ],
13
14
  "keywords": [
@@ -0,0 +1,294 @@
1
+ # 开发规范
2
+
3
+ ## 技术栈
4
+
5
+ - 前端: React 19 + TypeScript
6
+ - 后端: Express 5 + TypeScript
7
+ - 数据库: Drizzle ORM + PostgreSQL
8
+ - 样式: Tailwind CSS v4
9
+ - UI 组件: shadcn/ui `import { Button } from "@/components/ui/button";`
10
+ - 图标: lucide-react `import { SearchIcon } from "lucide-react";`
11
+ - 图表: recharts `import { LineChart } from "recharts";`
12
+ - 动画: framer-motion `import { motion } from "framer-motion";`
13
+ - 路由: react-router-dom `import { Link, useNavigate } from "react-router-dom";`
14
+ - 校验: zod `import { z } from "zod";`
15
+
16
+ ---
17
+
18
+ ## 项目结构
19
+
20
+ ```
21
+ ├── client/src/ # 前端代码
22
+ │ ├── app.tsx # 路由注册
23
+ │ ├── index.css # 全局样式 + 主题变量
24
+ │ ├── api/ # API 请求封装
25
+ │ │ └── index.ts
26
+ │ ├── components/ # 全局共享组件
27
+ │ │ └── ui/ # shadcn/ui 内置组件(勿手动修改)
28
+ │ ├── pages/ # 页面模块(每个页面一个目录)
29
+ │ │ ├── home/
30
+ │ │ └── todos/ # CRUD 示例
31
+ │ │ ├── index.tsx
32
+ │ │ └── components/
33
+ │ ├── hooks/ # 自定义 Hooks
34
+ │ └── lib/ # 工具函数(cn() 等)
35
+ ├── server/ # 后端代码
36
+ │ ├── index.ts # Express 入口(dev: Vite HMR 中间件)
37
+ │ ├── routes/ # API 路由
38
+ │ │ ├── index.ts # 路由注册
39
+ │ │ └── todos.ts # Todos CRUD 路由
40
+ │ └── db/ # 数据库层
41
+ │ ├── schema.ts # Drizzle schema 定义(可由工具生成)
42
+ │ └── index.ts # 数据库连接
43
+ ├── shared/ # 前后端共享(不依赖 client 或 server)
44
+ │ ├── types.ts # 数据模型类型
45
+ │ └── api.interface.ts # zod schema + API 入参/出参类型
46
+ ```
47
+
48
+ ---
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
+ ## 新增资源(以 posts 为例)
61
+
62
+ ### 1. shared/ — 定义类型和校验
63
+
64
+ `shared/types.ts` 增加数据模型:
65
+
66
+ ```typescript
67
+ export interface Post {
68
+ id: number;
69
+ title: string;
70
+ content: string | null;
71
+ createdAt: string;
72
+ }
73
+ ```
74
+
75
+ `shared/api.interface.ts` 增加 zod schema 和 API 类型:
76
+
77
+ ```typescript
78
+ export const createPostSchema = z.object({
79
+ title: z.string().min(1).max(200),
80
+ content: z.string().optional(),
81
+ });
82
+ export type CreatePostRequest = z.infer<typeof createPostSchema>;
83
+ export type CreatePostResponse = Post;
84
+ export type ListPostsResponse = Post[];
85
+ ```
86
+
87
+ ### 2. server/ — 数据库和路由
88
+
89
+ `server/db/schema.ts` 增加表定义:
90
+
91
+ ```typescript
92
+ export const posts = pgTable("posts", {
93
+ id: serial("id").primaryKey(),
94
+ title: text("title").notNull(),
95
+ content: text("content"),
96
+ createdAt: timestamp("created_at").notNull().defaultNow(),
97
+ });
98
+ ```
99
+
100
+ `server/routes/posts.ts` 编写 CRUD 路由:
101
+
102
+ ```typescript
103
+ import { Router } from "express";
104
+ import { db } from "../db/index";
105
+ import { posts } from "../db/schema";
106
+ import { createPostSchema } from "../../shared/api.interface";
107
+
108
+ const router = Router();
109
+ router.get("/", async (_req, res) => {
110
+ const list = await db.select().from(posts);
111
+ res.json(list);
112
+ });
113
+ router.post("/", async (req, res) => {
114
+ const parsed = createPostSchema.safeParse(req.body);
115
+ if (!parsed.success) { res.status(400).json({ error: parsed.error.flatten() }); return; }
116
+ const [post] = await db.insert(posts).values(parsed.data).returning();
117
+ res.status(201).json(post);
118
+ });
119
+ export default router;
120
+ ```
121
+
122
+ `server/routes/index.ts` 注册:
123
+
124
+ ```typescript
125
+ import postsRouter from "./posts";
126
+ app.use("/api/posts", postsRouter);
127
+ ```
128
+
129
+ ### 3. client/ — API 封装和页面
130
+
131
+ `client/src/api/index.ts` 增加封装:
132
+
133
+ ```typescript
134
+ import type { CreatePostRequest, CreatePostResponse, ListPostsResponse } from "@shared/api.interface";
135
+
136
+ export const postsApi = {
137
+ list: () => request<ListPostsResponse>("/api/posts"),
138
+ create: (data: CreatePostRequest) => request<CreatePostResponse>("/api/posts", {
139
+ method: "POST", body: JSON.stringify(data),
140
+ }),
141
+ };
142
+ ```
143
+
144
+ `client/src/pages/posts/index.tsx` 编写页面,`client/src/app.tsx` 注册路由。
145
+
146
+ ---
147
+
148
+ ## 页面与组件规范
149
+
150
+ **页面文件只做骨架编排,不包含具体 UI 实现。**
151
+
152
+ ```tsx
153
+ // client/src/pages/dashboard/index.tsx
154
+ import { StatsSection } from "./components/stats-section";
155
+ import { DataTableSection } from "./components/data-table-section";
156
+
157
+ export default function DashboardPage() {
158
+ return (
159
+ <div className="space-y-8">
160
+ <StatsSection />
161
+ <DataTableSection />
162
+ </div>
163
+ );
164
+ }
165
+ ```
166
+
167
+ **规则:**
168
+
169
+ - 每个视觉上独立的区块拆为一个组件文件,即使只出现一次
170
+ - 单个组件文件不超过 **150 行**,超出时进一步拆分子组件
171
+ - 页面专属组件放在 `pages/<page>/components/`
172
+ - 跨页面复用的组件放在 `client/src/components/`
173
+ - 相同 UI 片段出现 **≥2 次**时,必须提取为可复用组件
174
+ - 文件名 kebab-case(`stat-card.tsx`),组件名 PascalCase(`StatCard`)
175
+ - 组件之间**禁止循环引用**
176
+
177
+ ---
178
+
179
+ ## 路由注册
180
+
181
+ 新增页面在 `client/src/app.tsx` 中注册:
182
+
183
+ ```tsx
184
+ <Route element={<Layout />}>
185
+ <Route index element={<HomePage />} />
186
+ <Route path="todos" element={<TodosPage />} />
187
+ <Route path="dashboard" element={<DashboardPage />} />
188
+ <Route path="*" element={<NotFoundPage />} />
189
+ </Route>
190
+ ```
191
+
192
+ **新增页面步骤:**
193
+
194
+ 1. 在 `client/src/pages/` 下新建页面目录和 `index.tsx`
195
+ 2. 在 `app.tsx` 的 `<Routes>` 内添加 `<Route>` 配置
196
+
197
+ **路由跳转必须使用 react-router-dom:**
198
+
199
+ - 组件内跳转:`<Link to="/dashboard">Dashboard</Link>`
200
+ - 编程式跳转:`const navigate = useNavigate(); navigate("/dashboard");`
201
+ - **禁止使用** `<a href="/">` 或 `window.location` 进行页面内跳转
202
+
203
+ ---
204
+
205
+ ## 样式与主题
206
+
207
+ ### 主题变量
208
+
209
+ 主题色定义在 `client/src/index.css` 中,通过 `:root` CSS 变量 + `@theme inline` 注册到 Tailwind。
210
+
211
+ **语义化颜色对照:**
212
+
213
+ | 用途 | Tailwind 类 | CSS 变量 |
214
+ |------|------------|----------|
215
+ | 页面背景 | `bg-background` | `--background` |
216
+ | 主文本 | `text-foreground` | `--foreground` |
217
+ | 卡片背景 | `bg-card` | `--card` |
218
+ | 次要文本 | `text-muted-foreground` | `--muted-foreground` |
219
+ | 主色 | `bg-primary` / `text-primary` | `--primary` |
220
+ | 强调色 | `bg-accent` | `--accent` |
221
+ | 边框 | `border-border` | `--border` |
222
+ | 危险色 | `text-destructive` | `--destructive` |
223
+ | 成功色 | `text-success-foreground` | `--success-foreground` |
224
+ | 警告色 | `text-warning-foreground` | `--warning-foreground` |
225
+ | 图表色 | `bg-chart-1` ~ `bg-chart-5` | `--chart-1` ~ `--chart-5` |
226
+
227
+ **颜色使用规则:**
228
+
229
+ - 主题色(背景、文本、主色、边框等)**必须使用语义化变量类**
230
+ - 灰阶辅助色(细节装饰、次要分隔线)可使用 Tailwind 原生色(如 `text-gray-500`)
231
+ - 类名合并使用 `cn()`:`import { cn } from "@/lib/utils"`
232
+
233
+ ### 主题增量修改规范
234
+
235
+ 修改主题时,**只覆盖需要变更的变量**:
236
+
237
+ ```css
238
+ /* 正确:仅修改需要的变量 */
239
+ :root {
240
+ --primary: hsl(150, 60%, 40%);
241
+ --primary-foreground: hsl(0, 0%, 100%);
242
+ }
243
+
244
+ /* 禁止:复制整个 :root 块后修改 */
245
+ ```
246
+
247
+ - 新增自定义颜色变量时,必须同时在 `:root` 和 `@theme inline` 中注册
248
+ - 禁止直接修改 `@theme inline` 中已有的 `--color-*` 映射关系
249
+ - 禁止删除已有的主题变量(可能被 shadcn/ui 组件依赖)
250
+
251
+ ---
252
+
253
+ ## 布局与交互
254
+
255
+ ### 响应式布局
256
+
257
+ - 容器使用 `max-w-*` + `mx-auto` 居中,禁止内容在大屏贴边延伸
258
+ - 多列布局使用 `grid` + 断点类:`grid-cols-1 md:grid-cols-2 lg:grid-cols-3`
259
+ - flex 子元素设置 `min-w-0`,多元素横排时加 `flex-wrap`
260
+ - 禁止固定像素宽度作为主容器(如 `w-[720px]`)
261
+
262
+ ### 内容自适应
263
+
264
+ - 区块高度由内容撑开,禁止固定 `h-` 值(图表容器除外)
265
+ - 图片:`max-w-full h-auto`
266
+ - 长文本:`break-words`
267
+ - 单行截断:`truncate`
268
+ - 表格/代码块:`overflow-x-auto`
269
+
270
+ ### 交互规范
271
+
272
+ - 所有交互元素(按钮、链接、标签页等)必须有**实际交互逻辑**和**可见反馈**
273
+ - 禁止空函数(`onClick={() => {}}`)或仅 `console.log` 的响应
274
+ - 禁止 `href="#"` 占位链接、无内容切换的标签页、空下拉菜单
275
+ - 禁止"导出"、"分享"等无法真正执行的操作按钮
276
+ - 如功能暂未实现,**删除该入口**,不实现假按钮
277
+
278
+ ---
279
+
280
+ ## 自检清单
281
+
282
+ | 检查项 | 验收标准 |
283
+ |--------|---------|
284
+ | 页面拆分 | 页面文件只做骨架编排;每个区块为独立组件;单文件 ≤150 行 |
285
+ | 组件复用 | 相同片段 ≥2 次已提取为组件;文件名 kebab-case,组件名 PascalCase |
286
+ | 路由注册 | 新页面已在 `app.tsx` 注册;跳转使用 `<Link>` / `useNavigate()`,无 `<a href>` |
287
+ | API 调用 | 统一在 `api/` 封装;使用 `@shared` 类型;组件内不直接 fetch |
288
+ | 输入校验 | zod schema 定义在 `shared/api.interface.ts`;server 和 client 共用 |
289
+ | 主题色 | 使用语义化变量类(`bg-background`、`text-primary` 等);未硬编码颜色值 |
290
+ | 主题修改 | 仅增量覆盖变更的变量;新增色同时注册 `:root` 和 `@theme inline` |
291
+ | 响应式 | 容器 `max-w-*` + `mx-auto`;多列布局窄屏退化单列;flex 有 `min-w-0` |
292
+ | 内容自适应 | 无固定 `h-`(图表除外);长文本 `break-words`;表格 `overflow-x-auto` |
293
+ | 交互完整性 | 所有按钮/链接有实际处理器和可见反馈;无空响应、无假按钮 |
294
+ | 无循环引用 | client ↔ server 不直接 import;通过 shared/ 共享类型 |
@@ -0,0 +1 @@
1
+ DATABASE_URL=postgresql://postgres:postgres@localhost:5432/openclaw
@@ -0,0 +1,27 @@
1
+ # Logs
2
+ logs
3
+ *.log
4
+ npm-debug.log*
5
+ yarn-debug.log*
6
+ yarn-error.log*
7
+ pnpm-debug.log*
8
+ lerna-debug.log*
9
+
10
+ node_modules
11
+ dist
12
+ dist-ssr
13
+ *.local
14
+
15
+ # Database
16
+ drizzle/meta
17
+
18
+ # Editor directories and files
19
+ .vscode/*
20
+ !.vscode/extensions.json
21
+ .idea
22
+ .DS_Store
23
+ *.suo
24
+ *.ntvs*
25
+ *.njsproj
26
+ *.sln
27
+ *.sw?
@@ -0,0 +1,20 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <link rel="icon" type="image/svg+xml" href="/favicon.svg" />
6
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
7
+ <title>应用标题</title>
8
+ <link href="https://lf3-static.bytednsdoc.com/obj/eden-cn/ylcylz_fsph_ryhs/ljhwZthlaukjlkulzlp/feisuda/feisuda.svg" rel="shortcut icon"/>
9
+ </head>
10
+ <body>
11
+ <script>
12
+ window.__APP_CONFIG__ = {
13
+ appId: "{{{appId}}}",
14
+ cdnDomain: "{{{cdnDomain}}}"
15
+ };
16
+ </script>
17
+ <div id="root"></div>
18
+ <script type="module" src="/src/main.tsx"></script>
19
+ </body>
20
+ </html>
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" width="48" height="46" fill="none" viewBox="0 0 48 46"><path fill="#863bff" d="M25.946 44.938c-.664.845-2.021.375-2.021-.698V33.937a2.26 2.26 0 0 0-2.262-2.262H10.287c-.92 0-1.456-1.04-.92-1.788l7.48-10.471c1.07-1.497 0-3.578-1.842-3.578H1.237c-.92 0-1.456-1.04-.92-1.788L10.013.474c.214-.297.556-.474.92-.474h28.894c.92 0 1.456 1.04.92 1.788l-7.48 10.471c-1.07 1.498 0 3.579 1.842 3.579h11.377c.943 0 1.473 1.088.89 1.83L25.947 44.94z" style="fill:#863bff;fill:color(display-p3 .5252 .23 1);fill-opacity:1"/><mask id="a" width="48" height="46" x="0" y="0" maskUnits="userSpaceOnUse" style="mask-type:alpha"><path fill="#000" d="M25.842 44.938c-.664.844-2.021.375-2.021-.698V33.937a2.26 2.26 0 0 0-2.262-2.262H10.183c-.92 0-1.456-1.04-.92-1.788l7.48-10.471c1.07-1.498 0-3.579-1.842-3.579H1.133c-.92 0-1.456-1.04-.92-1.787L9.91.473c.214-.297.556-.474.92-.474h28.894c.92 0 1.456 1.04.92 1.788l-7.48 10.471c-1.07 1.498 0 3.578 1.842 3.578h11.377c.943 0 1.473 1.088.89 1.832L25.843 44.94z" style="fill:#000;fill-opacity:1"/></mask><g mask="url(#a)"><g filter="url(#b)"><ellipse cx="5.508" cy="14.704" fill="#ede6ff" rx="5.508" ry="14.704" style="fill:#ede6ff;fill:color(display-p3 .9275 .9033 1);fill-opacity:1" transform="matrix(.00324 1 1 -.00324 -4.47 31.516)"/></g><g filter="url(#c)"><ellipse cx="10.399" cy="29.851" fill="#ede6ff" rx="10.399" ry="29.851" style="fill:#ede6ff;fill:color(display-p3 .9275 .9033 1);fill-opacity:1" transform="matrix(.00324 1 1 -.00324 -39.328 7.883)"/></g><g filter="url(#d)"><ellipse cx="5.508" cy="30.487" fill="#7e14ff" rx="5.508" ry="30.487" style="fill:#7e14ff;fill:color(display-p3 .4922 .0767 1);fill-opacity:1" transform="rotate(89.814 -25.913 -14.639)scale(1 -1)"/></g><g filter="url(#e)"><ellipse cx="5.508" cy="30.599" fill="#7e14ff" rx="5.508" ry="30.599" style="fill:#7e14ff;fill:color(display-p3 .4922 .0767 1);fill-opacity:1" transform="rotate(89.814 -32.644 -3.334)scale(1 -1)"/></g><g filter="url(#f)"><ellipse cx="5.508" cy="30.599" fill="#7e14ff" rx="5.508" ry="30.599" style="fill:#7e14ff;fill:color(display-p3 .4922 .0767 1);fill-opacity:1" transform="matrix(.00324 1 1 -.00324 -34.34 30.47)"/></g><g filter="url(#g)"><ellipse cx="14.072" cy="22.078" fill="#ede6ff" rx="14.072" ry="22.078" style="fill:#ede6ff;fill:color(display-p3 .9275 .9033 1);fill-opacity:1" transform="rotate(93.35 24.506 48.493)scale(-1 1)"/></g><g filter="url(#h)"><ellipse cx="3.47" cy="21.501" fill="#7e14ff" rx="3.47" ry="21.501" style="fill:#7e14ff;fill:color(display-p3 .4922 .0767 1);fill-opacity:1" transform="rotate(89.009 28.708 47.59)scale(-1 1)"/></g><g filter="url(#i)"><ellipse cx="3.47" cy="21.501" fill="#7e14ff" rx="3.47" ry="21.501" style="fill:#7e14ff;fill:color(display-p3 .4922 .0767 1);fill-opacity:1" transform="rotate(89.009 28.708 47.59)scale(-1 1)"/></g><g filter="url(#j)"><ellipse cx=".387" cy="8.972" fill="#7e14ff" rx="4.407" ry="29.108" style="fill:#7e14ff;fill:color(display-p3 .4922 .0767 1);fill-opacity:1" transform="rotate(39.51 .387 8.972)"/></g><g filter="url(#k)"><ellipse cx="47.523" cy="-6.092" fill="#7e14ff" rx="4.407" ry="29.108" style="fill:#7e14ff;fill:color(display-p3 .4922 .0767 1);fill-opacity:1" transform="rotate(37.892 47.523 -6.092)"/></g><g filter="url(#l)"><ellipse cx="41.412" cy="6.333" fill="#47bfff" rx="5.971" ry="9.665" style="fill:#47bfff;fill:color(display-p3 .2799 .748 1);fill-opacity:1" transform="rotate(37.892 41.412 6.333)"/></g><g filter="url(#m)"><ellipse cx="-1.879" cy="38.332" fill="#7e14ff" rx="4.407" ry="29.108" style="fill:#7e14ff;fill:color(display-p3 .4922 .0767 1);fill-opacity:1" transform="rotate(37.892 -1.88 38.332)"/></g><g filter="url(#n)"><ellipse cx="-1.879" cy="38.332" fill="#7e14ff" rx="4.407" ry="29.108" style="fill:#7e14ff;fill:color(display-p3 .4922 .0767 1);fill-opacity:1" transform="rotate(37.892 -1.88 38.332)"/></g><g filter="url(#o)"><ellipse cx="35.651" cy="29.907" fill="#7e14ff" rx="4.407" ry="29.108" style="fill:#7e14ff;fill:color(display-p3 .4922 .0767 1);fill-opacity:1" transform="rotate(37.892 35.651 29.907)"/></g><g filter="url(#p)"><ellipse cx="38.418" cy="32.4" fill="#47bfff" rx="5.971" ry="15.297" style="fill:#47bfff;fill:color(display-p3 .2799 .748 1);fill-opacity:1" transform="rotate(37.892 38.418 32.4)"/></g></g><defs><filter id="b" width="60.045" height="41.654" x="-19.77" y="16.149" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17158" stdDeviation="7.659"/></filter><filter id="c" width="90.34" height="51.437" x="-54.613" y="-7.533" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17158" stdDeviation="7.659"/></filter><filter id="d" width="79.355" height="29.4" x="-49.64" y="2.03" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17158" stdDeviation="4.596"/></filter><filter id="e" width="79.579" height="29.4" x="-45.045" y="20.029" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17158" stdDeviation="4.596"/></filter><filter id="f" width="79.579" height="29.4" x="-43.513" y="21.178" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17158" stdDeviation="4.596"/></filter><filter id="g" width="74.749" height="58.852" x="15.756" y="-17.901" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17158" stdDeviation="7.659"/></filter><filter id="h" width="61.377" height="25.362" x="23.548" y="2.284" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17158" stdDeviation="4.596"/></filter><filter id="i" width="61.377" height="25.362" x="23.548" y="2.284" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17158" stdDeviation="4.596"/></filter><filter id="j" width="56.045" height="63.649" x="-27.636" y="-22.853" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17158" stdDeviation="4.596"/></filter><filter id="k" width="54.814" height="64.646" x="20.116" y="-38.415" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17158" stdDeviation="4.596"/></filter><filter id="l" width="33.541" height="35.313" x="24.641" y="-11.323" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17158" stdDeviation="4.596"/></filter><filter id="m" width="54.814" height="64.646" x="-29.286" y="6.009" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17158" stdDeviation="4.596"/></filter><filter id="n" width="54.814" height="64.646" x="-29.286" y="6.009" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17158" stdDeviation="4.596"/></filter><filter id="o" width="54.814" height="64.646" x="8.244" y="-2.416" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17158" stdDeviation="4.596"/></filter><filter id="p" width="39.409" height="43.623" x="18.713" y="10.588" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17158" stdDeviation="4.596"/></filter></defs></svg>
@@ -0,0 +1,24 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg">
2
+ <symbol id="bluesky-icon" viewBox="0 0 16 17">
3
+ <g clip-path="url(#bluesky-clip)"><path fill="#08060d" d="M7.75 7.735c-.693-1.348-2.58-3.86-4.334-5.097-1.68-1.187-2.32-.981-2.74-.79C.188 2.065.1 2.812.1 3.251s.241 3.602.398 4.13c.52 1.744 2.367 2.333 4.07 2.145-2.495.37-4.71 1.278-1.805 4.512 3.196 3.309 4.38-.71 4.987-2.746.608 2.036 1.307 5.91 4.93 2.746 2.72-2.746.747-4.143-1.747-4.512 1.702.189 3.55-.4 4.07-2.145.156-.528.397-3.691.397-4.13s-.088-1.186-.575-1.406c-.42-.19-1.06-.395-2.741.79-1.755 1.24-3.64 3.752-4.334 5.099"/></g>
4
+ <defs><clipPath id="bluesky-clip"><path fill="#fff" d="M.1.85h15.3v15.3H.1z"/></clipPath></defs>
5
+ </symbol>
6
+ <symbol id="discord-icon" viewBox="0 0 20 19">
7
+ <path fill="#08060d" d="M16.224 3.768a14.5 14.5 0 0 0-3.67-1.153c-.158.286-.343.67-.47.976a13.5 13.5 0 0 0-4.067 0c-.128-.306-.317-.69-.476-.976A14.4 14.4 0 0 0 3.868 3.77C1.546 7.28.916 10.703 1.231 14.077a14.7 14.7 0 0 0 4.5 2.306q.545-.748.965-1.587a9.5 9.5 0 0 1-1.518-.74q.191-.14.372-.293c2.927 1.369 6.107 1.369 8.999 0q.183.152.372.294-.723.437-1.52.74.418.838.963 1.588a14.6 14.6 0 0 0 4.504-2.308c.37-3.911-.63-7.302-2.644-10.309m-9.13 8.234c-.878 0-1.599-.82-1.599-1.82 0-.998.705-1.82 1.6-1.82.894 0 1.614.82 1.599 1.82.001 1-.705 1.82-1.6 1.82m5.91 0c-.878 0-1.599-.82-1.599-1.82 0-.998.705-1.82 1.6-1.82.893 0 1.614.82 1.599 1.82 0 1-.706 1.82-1.6 1.82"/>
8
+ </symbol>
9
+ <symbol id="documentation-icon" viewBox="0 0 21 20">
10
+ <path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="m15.5 13.333 1.533 1.322c.645.555.967.833.967 1.178s-.322.623-.967 1.179L15.5 18.333m-3.333-5-1.534 1.322c-.644.555-.966.833-.966 1.178s.322.623.966 1.179l1.534 1.321"/>
11
+ <path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="M17.167 10.836v-4.32c0-1.41 0-2.117-.224-2.68-.359-.906-1.118-1.621-2.08-1.96-.599-.21-1.349-.21-2.848-.21-2.623 0-3.935 0-4.983.369-1.684.591-3.013 1.842-3.641 3.428C3 6.449 3 7.684 3 10.154v2.122c0 2.558 0 3.838.706 4.726q.306.383.713.671c.76.536 1.79.64 3.581.66"/>
12
+ <path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="M3 10a2.78 2.78 0 0 1 2.778-2.778c.555 0 1.209.097 1.748-.047.48-.129.854-.503.982-.982.145-.54.048-1.194.048-1.749a2.78 2.78 0 0 1 2.777-2.777"/>
13
+ </symbol>
14
+ <symbol id="github-icon" viewBox="0 0 19 19">
15
+ <path fill="#08060d" fill-rule="evenodd" d="M9.356 1.85C5.05 1.85 1.57 5.356 1.57 9.694a7.84 7.84 0 0 0 5.324 7.44c.387.079.528-.168.528-.376 0-.182-.013-.805-.013-1.454-2.165.467-2.616-.935-2.616-.935-.349-.91-.864-1.143-.864-1.143-.71-.48.051-.48.051-.48.787.051 1.2.805 1.2.805.695 1.194 1.817.857 2.268.649.064-.507.27-.857.49-1.052-1.728-.182-3.545-.857-3.545-3.87 0-.857.31-1.558.8-2.104-.078-.195-.349-1 .077-2.078 0 0 .657-.208 2.14.805a7.5 7.5 0 0 1 1.946-.26c.657 0 1.328.092 1.946.26 1.483-1.013 2.14-.805 2.14-.805.426 1.078.155 1.883.078 2.078.502.546.799 1.247.799 2.104 0 3.013-1.818 3.675-3.558 3.87.284.247.528.714.528 1.454 0 1.052-.012 1.896-.012 2.156 0 .208.142.455.528.377a7.84 7.84 0 0 0 5.324-7.441c.013-4.338-3.48-7.844-7.773-7.844" clip-rule="evenodd"/>
16
+ </symbol>
17
+ <symbol id="social-icon" viewBox="0 0 20 20">
18
+ <path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="M12.5 6.667a4.167 4.167 0 1 0-8.334 0 4.167 4.167 0 0 0 8.334 0"/>
19
+ <path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="M2.5 16.667a5.833 5.833 0 0 1 8.75-5.053m3.837.474.513 1.035c.07.144.257.282.414.309l.93.155c.596.1.736.536.307.965l-.723.73a.64.64 0 0 0-.152.531l.207.903c.164.715-.213.991-.84.618l-.872-.52a.63.63 0 0 0-.577 0l-.872.52c-.624.373-1.003.094-.84-.618l.207-.903a.64.64 0 0 0-.152-.532l-.723-.729c-.426-.43-.289-.864.306-.964l.93-.156a.64.64 0 0 0 .412-.31l.513-1.034c.28-.562.735-.562 1.012 0"/>
20
+ </symbol>
21
+ <symbol id="x-icon" viewBox="0 0 19 19">
22
+ <path fill="#08060d" fill-rule="evenodd" d="M1.893 1.98c.052.072 1.245 1.769 2.653 3.77l2.892 4.114c.183.261.333.48.333.486s-.068.089-.152.183l-.522.593-.765.867-3.597 4.087c-.375.426-.734.834-.798.905a1 1 0 0 0-.118.148c0 .01.236.017.664.017h.663l.729-.83c.4-.457.796-.906.879-.999a692 692 0 0 0 1.794-2.038c.034-.037.301-.34.594-.675l.551-.624.345-.392a7 7 0 0 1 .34-.374c.006 0 .93 1.306 2.052 2.903l2.084 2.965.045.063h2.275c1.87 0 2.273-.003 2.266-.021-.008-.02-1.098-1.572-3.894-5.547-2.013-2.862-2.28-3.246-2.273-3.266.008-.019.282-.332 2.085-2.38l2-2.274 1.567-1.782c.022-.028-.016-.03-.65-.03h-.674l-.3.342a871 871 0 0 1-1.782 2.025c-.067.075-.405.458-.75.852a100 100 0 0 1-.803.91c-.148.172-.299.344-.99 1.127-.304.343-.32.358-.345.327-.015-.019-.904-1.282-1.976-2.808L6.365 1.85H1.8zm1.782.91 8.078 11.294c.772 1.08 1.413 1.973 1.425 1.984.016.017.241.02 1.05.017l1.03-.004-2.694-3.766L7.796 5.75 5.722 2.852l-1.039-.004-1.039-.004z" clip-rule="evenodd"/>
23
+ </symbol>
24
+ </svg>
@@ -0,0 +1,39 @@
1
+ import type {
2
+ CreateTodoRequest,
3
+ CreateTodoResponse,
4
+ UpdateTodoRequest,
5
+ UpdateTodoResponse,
6
+ DeleteTodoResponse,
7
+ ListTodosResponse,
8
+ } from "@shared/api.interface";
9
+
10
+ async function request<T>(url: string, options?: RequestInit): Promise<T> {
11
+ const res = await fetch(url, {
12
+ headers: { "Content-Type": "application/json" },
13
+ ...options,
14
+ });
15
+ if (!res.ok) {
16
+ const body = await res.json().catch(() => ({}));
17
+ throw new Error(body.error?.toString() ?? `Request failed: ${res.status}`);
18
+ }
19
+ return res.json();
20
+ }
21
+
22
+ export const todosApi = {
23
+ list: () => request<ListTodosResponse>("/api/todos"),
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
+ };
@@ -0,0 +1,19 @@
1
+ import { BrowserRouter, Routes, Route } from "react-router-dom";
2
+ import { Layout } from "@/components/layout";
3
+ import HomePage from "@/pages/home";
4
+ import TodosPage from "@/pages/todos";
5
+ import NotFoundPage from "@/pages/not-found";
6
+
7
+ export default function App() {
8
+ return (
9
+ <BrowserRouter basename={process.env.CLIENT_BASE_PATH || '/'}>
10
+ <Routes>
11
+ <Route element={<Layout />}>
12
+ <Route index element={<HomePage />} />
13
+ <Route path="todos" element={<TodosPage />} />
14
+ <Route path="*" element={<NotFoundPage />} />
15
+ </Route>
16
+ </Routes>
17
+ </BrowserRouter>
18
+ );
19
+ }
@@ -0,0 +1,11 @@
1
+ import { Outlet } from "react-router-dom";
2
+
3
+ export function Layout() {
4
+ return (
5
+ <div className="min-h-screen bg-background text-foreground">
6
+ <main className="container mx-auto px-4 py-8">
7
+ <Outlet />
8
+ </main>
9
+ </div>
10
+ );
11
+ }
@@ -0,0 +1,66 @@
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+ import * as AccordionPrimitive from "@radix-ui/react-accordion"
5
+ import { ChevronDownIcon } from "lucide-react"
6
+
7
+ import { cn } from "@/lib/utils"
8
+
9
+ function Accordion({
10
+ ...props
11
+ }: React.ComponentProps<typeof AccordionPrimitive.Root>) {
12
+ return <AccordionPrimitive.Root data-slot="accordion" {...props} />
13
+ }
14
+
15
+ function AccordionItem({
16
+ className,
17
+ ...props
18
+ }: React.ComponentProps<typeof AccordionPrimitive.Item>) {
19
+ return (
20
+ <AccordionPrimitive.Item
21
+ data-slot="accordion-item"
22
+ className={cn("border-b last:border-b-0", className)}
23
+ {...props}
24
+ />
25
+ )
26
+ }
27
+
28
+ function AccordionTrigger({
29
+ className,
30
+ children,
31
+ ...props
32
+ }: React.ComponentProps<typeof AccordionPrimitive.Trigger>) {
33
+ return (
34
+ <AccordionPrimitive.Header className="flex">
35
+ <AccordionPrimitive.Trigger
36
+ data-slot="accordion-trigger"
37
+ className={cn(
38
+ "focus-visible:border-ring focus-visible:ring-ring/20 flex flex-1 items-start justify-between gap-4 rounded-md py-4 text-left text-sm font-medium transition-all outline-none enabled:hover:underline focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 [&[data-state=open]>svg]:rotate-180",
39
+ className
40
+ )}
41
+ {...props}
42
+ >
43
+ {children}
44
+ <ChevronDownIcon className="text-muted-foreground pointer-events-none size-4 shrink-0 translate-y-0.5 transition-transform duration-200" />
45
+ </AccordionPrimitive.Trigger>
46
+ </AccordionPrimitive.Header>
47
+ )
48
+ }
49
+
50
+ function AccordionContent({
51
+ className,
52
+ children,
53
+ ...props
54
+ }: React.ComponentProps<typeof AccordionPrimitive.Content>) {
55
+ return (
56
+ <AccordionPrimitive.Content
57
+ data-slot="accordion-content"
58
+ className="data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down overflow-hidden text-sm"
59
+ {...props}
60
+ >
61
+ <div className={cn("pt-0 pb-4", className)}>{children}</div>
62
+ </AccordionPrimitive.Content>
63
+ )
64
+ }
65
+
66
+ export { Accordion, AccordionItem, AccordionTrigger, AccordionContent }