@nexttylabs/echo 0.3.0 → 0.5.0

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 (248) hide show
  1. package/CHANGELOG.md +25 -0
  2. package/app/(public)/[organizationSlug]/roadmap/page.tsx +19 -1
  3. package/app/api/admin/backup/route.ts +22 -4
  4. package/app/api/auth/register/handler.ts +1 -2
  5. package/lib/auth/config.ts +0 -7
  6. package/lib/db/migrations/0000_needy_leech.sql +335 -0
  7. package/lib/db/migrations/meta/0000_snapshot.json +2186 -1
  8. package/lib/db/migrations/meta/_journal.json +2 -135
  9. package/lib/db/schema/auth.ts +0 -1
  10. package/lib/db/schema/index.ts +0 -1
  11. package/lib/portal/public-context.tsx +5 -0
  12. package/package.json +20 -1
  13. package/.changeset/README.md +0 -21
  14. package/.changeset/config.json +0 -11
  15. package/.changeset/cozy-ghosts-care.md +0 -5
  16. package/.changeset/sharp-lines-stand.md +0 -5
  17. package/.changeset/sour-doodles-eat.md +0 -5
  18. package/.changeset/tender-moose-shop.md +0 -5
  19. package/.github/pull_request_template.md +0 -13
  20. package/.github/workflows/ci.yml +0 -41
  21. package/.github/workflows/publish.yml +0 -44
  22. package/.github/workflows/release.yml +0 -73
  23. package/AGENTS.md +0 -92
  24. package/Dockerfile +0 -57
  25. package/Makefile +0 -77
  26. package/app/api/internal/domain-lookup/route.ts +0 -67
  27. package/bun.lock +0 -2503
  28. package/components/portal/project-switcher.tsx +0 -20
  29. package/docker-compose.dev.yml +0 -26
  30. package/docker-compose.yml +0 -98
  31. package/docs/architecture.md +0 -259
  32. package/docs/component-inventory.md +0 -261
  33. package/docs/database-migrations.md +0 -76
  34. package/docs/development-guide.md +0 -209
  35. package/docs/e2e-user-flows.csv +0 -31
  36. package/docs/er-diagram-feedback.mmd +0 -138
  37. package/docs/er-diagram.mmd +0 -281
  38. package/docs/i18n-check-report.md +0 -296
  39. package/docs/index.md +0 -214
  40. package/docs/logic-chain.md +0 -94
  41. package/docs/plans/2026-01-02-database-migration-scripts.md +0 -496
  42. package/docs/plans/2026-01-02-user-login-design.md +0 -37
  43. package/docs/plans/2026-01-02-user-login.md +0 -437
  44. package/docs/plans/2026-01-02-user-registration-design.md +0 -47
  45. package/docs/plans/2026-01-02-user-registration.md +0 -628
  46. package/docs/plans/2026-01-03-roles-permissions-design.md +0 -20
  47. package/docs/plans/2026-01-03-roles-permissions.md +0 -266
  48. package/docs/plans/2026-01-05-authentication-middleware.md +0 -207
  49. package/docs/plans/2026-01-05-member-removal.md +0 -186
  50. package/docs/plans/2026-01-05-organization-creation.md +0 -374
  51. package/docs/plans/2026-01-05-rbac-middleware.md +0 -112
  52. package/docs/plans/2026-01-05-role-configuration.md +0 -441
  53. package/docs/plans/2026-01-06-file-upload-support.md +0 -804
  54. package/docs/plans/2026-01-06-permission-check-hook.md +0 -155
  55. package/docs/plans/2026-01-06-resource-ownership-check.md +0 -231
  56. package/docs/plans/2026-01-07-feedback-tracking-link.md +0 -459
  57. package/docs/plans/2026-01-09-logout-redirect-design.md +0 -52
  58. package/docs/plans/2026-01-09-phase2-3-plan.md +0 -654
  59. package/docs/plans/2026-01-09-portal-execution-plan.md +0 -408
  60. package/docs/plans/2026-01-09-project-delete-feature-design.md +0 -163
  61. package/docs/plans/2026-01-09-project-delete-implementation.md +0 -451
  62. package/docs/plans/2026-01-09-project-edit-delete-design.md +0 -52
  63. package/docs/plans/2026-01-09-settings-center-design.md +0 -114
  64. package/docs/plans/2026-01-09-settings-center.md +0 -948
  65. package/docs/plans/2026-01-10-organization-only-design.md +0 -66
  66. package/docs/plans/2026-01-10-organization-only-implementation.md +0 -433
  67. package/docs/plans/2026-01-10-portal-settings-restructure-plan.md +0 -18
  68. package/docs/plans/2026-01-10-project-settings-tabs-design-implementation.md +0 -296
  69. package/docs/plans/2026-01-14-e2e-playwright-feedback.md +0 -173
  70. package/docs/plans/2026-01-15-feedback-management-org-context-design.md +0 -82
  71. package/docs/plans/2026-01-15-feedback-management-org-context-implementation-plan.md +0 -521
  72. package/docs/plans/2026-01-16-admin-feedback-filters-design.md +0 -75
  73. package/docs/plans/2026-01-16-admin-feedback-filters-implementation.md +0 -293
  74. package/docs/plans/2026-01-16-admin-feedback-route-consolidation.md +0 -180
  75. package/docs/plans/2026-01-16-e2e-test-fixes.md +0 -158
  76. package/docs/plans/2026-01-17-admin-feedback-filters.md +0 -214
  77. package/docs/plans/2026-01-17-admin-feedback-improvements.md +0 -453
  78. package/docs/plans/2026-01-18-changesets-design.md +0 -40
  79. package/docs/product_changes.md +0 -37
  80. package/docs/project-overview.md +0 -159
  81. package/docs/project-scan-report.json +0 -104
  82. package/docs/route-role-visibility.md +0 -51
  83. package/docs/source-tree-analysis.md +0 -150
  84. package/docs/testing/delete-project-manual-tests.md +0 -18
  85. package/docs/user-story-tracking.md +0 -191
  86. package/eslint.config.mjs +0 -19
  87. package/lib/db/migrations/.gitkeep +0 -0
  88. package/lib/db/migrations/0000_cynical_gladiator.sql +0 -53
  89. package/lib/db/migrations/0001_wandering_sunfire.sql +0 -27
  90. package/lib/db/migrations/0002_shallow_speedball.sql +0 -1
  91. package/lib/db/migrations/0003_add_org_description.sql +0 -1
  92. package/lib/db/migrations/0003_boring_wild_pack.sql +0 -13
  93. package/lib/db/migrations/0004_windy_tyrannus.sql +0 -27
  94. package/lib/db/migrations/0005_perpetual_doorman.sql +0 -5
  95. package/lib/db/migrations/0006_aberrant_captain_midlands.sql +0 -13
  96. package/lib/db/migrations/0007_clever_captain_cross.sql +0 -14
  97. package/lib/db/migrations/0008_sparkling_pandemic.sql +0 -2
  98. package/lib/db/migrations/0009_happy_black_tom.sql +0 -29
  99. package/lib/db/migrations/0010_kind_junta.sql +0 -8
  100. package/lib/db/migrations/0011_mute_squadron_supreme.sql +0 -25
  101. package/lib/db/migrations/0012_giant_power_man.sql +0 -24
  102. package/lib/db/migrations/0013_damp_titanium_man.sql +0 -17
  103. package/lib/db/migrations/0014_blue_alice.sql +0 -18
  104. package/lib/db/migrations/0015_webhook_tables.sql +0 -41
  105. package/lib/db/migrations/0016_github_integration.sql +0 -30
  106. package/lib/db/migrations/0016_overjoyed_ghost_rider.sql +0 -22
  107. package/lib/db/migrations/0017_slimy_inhumans.sql +0 -6
  108. package/lib/db/migrations/0018_same_spitfire.sql +0 -1
  109. package/lib/db/migrations/0019_jittery_loners.sql +0 -16
  110. package/lib/db/migrations/0019_remove_projects_add_org_settings.sql +0 -14
  111. package/lib/db/migrations/meta/0001_snapshot.json +0 -553
  112. package/lib/db/migrations/meta/0002_snapshot.json +0 -560
  113. package/lib/db/migrations/meta/0003_snapshot.json +0 -650
  114. package/lib/db/migrations/meta/0004_snapshot.json +0 -852
  115. package/lib/db/migrations/meta/0005_snapshot.json +0 -900
  116. package/lib/db/migrations/meta/0006_snapshot.json +0 -1011
  117. package/lib/db/migrations/meta/0007_snapshot.json +0 -1125
  118. package/lib/db/migrations/meta/0008_snapshot.json +0 -1146
  119. package/lib/db/migrations/meta/0009_snapshot.json +0 -1386
  120. package/lib/db/migrations/meta/0010_snapshot.json +0 -1419
  121. package/lib/db/migrations/meta/0011_snapshot.json +0 -1615
  122. package/lib/db/migrations/meta/0012_snapshot.json +0 -1805
  123. package/lib/db/migrations/meta/0013_snapshot.json +0 -1948
  124. package/lib/db/migrations/meta/0014_snapshot.json +0 -2082
  125. package/lib/db/migrations/meta/0015_snapshot.json +0 -2476
  126. package/lib/db/migrations/meta/0016_snapshot.json +0 -2633
  127. package/lib/db/migrations/meta/0017_snapshot.json +0 -2680
  128. package/lib/db/migrations/meta/0018_snapshot.json +0 -2686
  129. package/lib/db/migrations/meta/0019_snapshot.json +0 -2741
  130. package/lib/db/schema/projects.ts +0 -145
  131. package/lib/db/schema/user-profiles.ts +0 -31
  132. package/lib/validations/projects.ts +0 -49
  133. package/next-env.d.ts +0 -6
  134. package/playwright.config.ts +0 -44
  135. package/proxy.test.ts +0 -131
  136. package/proxy.ts +0 -190
  137. package/scripts/backup-db.sh +0 -57
  138. package/scripts/backup-db.ts +0 -24
  139. package/scripts/generate-openapi.ts +0 -22
  140. package/scripts/migration-helper.ts +0 -39
  141. package/scripts/pre-deploy.ts +0 -75
  142. package/scripts/restore-db.sh +0 -60
  143. package/scripts/rollback.ts +0 -72
  144. package/scripts/seed-tags.ts +0 -48
  145. package/tests/api/feedback-bulk.test.ts +0 -47
  146. package/tests/api/feedback-by-id.test.ts +0 -67
  147. package/tests/api/feedback-comments-route-import.test.ts +0 -26
  148. package/tests/api/feedback-create.test.ts +0 -71
  149. package/tests/api/feedback-delete.test.ts +0 -160
  150. package/tests/api/feedback-filter.test.ts +0 -250
  151. package/tests/api/feedback-list.test.ts +0 -234
  152. package/tests/api/feedback-route-assignee-condition.test.ts +0 -32
  153. package/tests/api/feedback-similar.test.ts +0 -46
  154. package/tests/api/feedback-sort.test.ts +0 -261
  155. package/tests/api/feedback-status-enum.test.ts +0 -49
  156. package/tests/api/feedback-status-filter.test.ts +0 -117
  157. package/tests/api/feedback-submit-on-behalf.test.ts +0 -269
  158. package/tests/api/feedback.test.ts +0 -175
  159. package/tests/api/identify-jwt.test.ts +0 -25
  160. package/tests/api/invitation-accept.test.ts +0 -213
  161. package/tests/api/organization-invitations.test.ts +0 -186
  162. package/tests/api/organization-members-list.test.ts +0 -79
  163. package/tests/api/organization-members.test.ts +0 -340
  164. package/tests/api/organizations.test.ts +0 -149
  165. package/tests/api/register.test.ts +0 -112
  166. package/tests/api/upload.test.ts +0 -103
  167. package/tests/api/vote.test.ts +0 -82
  168. package/tests/app/admin-feedback-detail-page.test.tsx +0 -25
  169. package/tests/app/admin-feedback-list-page.test.tsx +0 -25
  170. package/tests/app/admin-feedback-new-page.test.tsx +0 -25
  171. package/tests/app/health-route-helpers.test.ts +0 -27
  172. package/tests/app/login-page.test.ts +0 -26
  173. package/tests/app/portal-page.test.ts +0 -29
  174. package/tests/app/project-portal-overview.test.tsx +0 -25
  175. package/tests/app/widget-page-import.test.ts +0 -25
  176. package/tests/components/create-post-dialog-defaults.test.ts +0 -43
  177. package/tests/components/feedback/duplicate-suggestions-inline.test.tsx +0 -27
  178. package/tests/components/feedback/embedded-feedback-form.test.tsx +0 -96
  179. package/tests/components/feedback/feedback-detail.test.tsx +0 -25
  180. package/tests/components/feedback/feedback-stats.test.tsx +0 -49
  181. package/tests/components/feedback-bulk-actions.test.tsx +0 -39
  182. package/tests/components/feedback-i18n-keys.test.ts +0 -70
  183. package/tests/components/feedback-list-controls-compile.test.ts +0 -25
  184. package/tests/components/feedback-list-controls.test.tsx +0 -204
  185. package/tests/components/feedback-list-item.test.tsx +0 -67
  186. package/tests/components/landing/hero.test.tsx +0 -46
  187. package/tests/components/layout/language-switcher.test.tsx +0 -25
  188. package/tests/components/layout/sidebar.test.tsx +0 -157
  189. package/tests/components/login-form.test.ts +0 -25
  190. package/tests/components/organization-form.test.ts +0 -32
  191. package/tests/components/organization-switcher.test.ts +0 -25
  192. package/tests/components/pagination.test.tsx +0 -43
  193. package/tests/components/portal-overview.test.tsx +0 -25
  194. package/tests/components/profile-form.test.tsx +0 -139
  195. package/tests/components/role-selector.test.ts +0 -31
  196. package/tests/components/status-chart.test.tsx +0 -90
  197. package/tests/e2e/auth.e2e.ts +0 -323
  198. package/tests/e2e/feedback-actions.e2e.ts +0 -471
  199. package/tests/e2e/feedback-attachment.e2e.ts +0 -168
  200. package/tests/e2e/feedback-customer.e2e.ts +0 -226
  201. package/tests/e2e/feedback-management.e2e.ts +0 -565
  202. package/tests/e2e/feedback-submit.e2e.ts +0 -133
  203. package/tests/e2e/feedback-view.e2e.ts +0 -297
  204. package/tests/e2e/fixtures/test-data.ts +0 -235
  205. package/tests/e2e/health-check.e2e.ts +0 -230
  206. package/tests/e2e/helpers/test-utils-helpers.test.ts +0 -43
  207. package/tests/e2e/helpers/test-utils.ts +0 -298
  208. package/tests/e2e/integration-placeholders.e2e.ts +0 -199
  209. package/tests/e2e/organization.e2e.ts +0 -292
  210. package/tests/e2e/permissions.e2e.ts +0 -424
  211. package/tests/e2e/project-widget.e2e.ts +0 -63
  212. package/tests/feedback/filters.test.ts +0 -29
  213. package/tests/hooks/use-permissions.test.ts +0 -52
  214. package/tests/lib/ai/classifier.test.ts +0 -104
  215. package/tests/lib/ai/duplicate-detector.test.ts +0 -234
  216. package/tests/lib/attachments-schema.test.ts +0 -30
  217. package/tests/lib/auth/session.test.ts +0 -49
  218. package/tests/lib/auth-client.test.ts +0 -37
  219. package/tests/lib/auth-config.test.ts +0 -26
  220. package/tests/lib/feedback-prefill.test.ts +0 -52
  221. package/tests/lib/feedback-processor.test.ts +0 -41
  222. package/tests/lib/feedback-schema.test.ts +0 -33
  223. package/tests/lib/file-validator.test.ts +0 -48
  224. package/tests/lib/get-feedback-by-id.test.ts +0 -37
  225. package/tests/lib/invitations.test.ts +0 -35
  226. package/tests/lib/login-schema.test.ts +0 -36
  227. package/tests/lib/org-context.test.ts +0 -95
  228. package/tests/lib/organization-access.test.ts +0 -44
  229. package/tests/lib/organization-member-role-schema.test.ts +0 -41
  230. package/tests/lib/permissions.test.ts +0 -88
  231. package/tests/lib/portal-analytics.test.ts +0 -25
  232. package/tests/lib/portal-contributors.test.ts +0 -25
  233. package/tests/lib/portal-copy.test.ts +0 -27
  234. package/tests/lib/portal-i18n.test.ts +0 -30
  235. package/tests/lib/portal-leaderboard-settings.test.ts +0 -25
  236. package/tests/lib/portal-modules.test.ts +0 -25
  237. package/tests/lib/portal-seo.test.ts +0 -25
  238. package/tests/lib/portal-sharing.test.ts +0 -25
  239. package/tests/lib/portal-sorting.test.ts +0 -25
  240. package/tests/lib/portal-theme.test.ts +0 -25
  241. package/tests/lib/rate-limit.test.ts +0 -142
  242. package/tests/lib/resolve-locale.test.ts +0 -34
  243. package/tests/lib/services/backup.test.ts +0 -145
  244. package/tests/lib/user-organizations.test.ts +0 -42
  245. package/tests/lib/user-role-schema.test.ts +0 -33
  246. package/tests/lib/user-schema.test.ts +0 -25
  247. package/tests/setup.ts +0 -74
  248. package/vercel.json +0 -4
@@ -1,948 +0,0 @@
1
- # Settings Center Implementation Plan
2
-
3
- > **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
4
-
5
- **Goal:** Consolidate all settings into a unified Settings Center accessible from user dropdown menu.
6
-
7
- **Architecture:**
8
- - Remove settings links from main navigation, add user dropdown menu with Settings/Logout
9
- - Create `/settings` layout with left sidebar navigation and right content area
10
- - Reuse existing notification preferences component, create new profile/appearance pages
11
-
12
- **Tech Stack:** Next.js App Router, Radix UI DropdownMenu, Tailwind CSS
13
-
14
- ---
15
-
16
- ## Task 1: Simplify Sidebar Navigation
17
-
18
- **Files:**
19
- - Modify: `components/layout/sidebar.tsx`
20
-
21
- **Step 1: Remove settings-related nav items from navigation**
22
-
23
- Replace lines 55-63 with:
24
-
25
- ```tsx
26
- const navItems = [
27
- { href: "/dashboard", label: "仪表盘", icon: LayoutDashboard },
28
- { href: "/feedback", label: "反馈管理", icon: MessageSquare },
29
- ];
30
- ```
31
-
32
- Remove `adminNavItems` array entirely and its rendering (lines 61-63 and 110-124).
33
-
34
- **Step 2: Run build to verify no errors**
35
-
36
- Run: `bun run build 2>&1 | tail -10`
37
- Expected: Build succeeds
38
-
39
- **Step 3: Commit**
40
-
41
- ```bash
42
- git add components/layout/sidebar.tsx
43
- git commit -m "refactor: remove settings links from main navigation"
44
- ```
45
-
46
- ---
47
-
48
- ## Task 2: Add User Dropdown Menu to Sidebar
49
-
50
- **Files:**
51
- - Modify: `components/layout/sidebar.tsx`
52
-
53
- **Step 1: Add dropdown menu imports**
54
-
55
- Add to imports:
56
-
57
- ```tsx
58
- import {
59
- DropdownMenu,
60
- DropdownMenuContent,
61
- DropdownMenuItem,
62
- DropdownMenuSeparator,
63
- DropdownMenuTrigger,
64
- } from "@/components/ui/dropdown-menu";
65
- import { ChevronUp } from "lucide-react";
66
- ```
67
-
68
- **Step 2: Replace user profile section (lines 165-191)**
69
-
70
- Replace the entire `{/* User Profile */}` section with:
71
-
72
- ```tsx
73
- {/* User Profile Dropdown */}
74
- <div className="border-t p-4">
75
- <DropdownMenu>
76
- <DropdownMenuTrigger asChild>
77
- <button className="flex w-full items-center gap-3 rounded-lg px-2 py-2 hover:bg-muted transition-colors">
78
- <Avatar className="h-9 w-9">
79
- <AvatarImage src={user.image || undefined} alt={user.name} />
80
- <AvatarFallback>{user.name.slice(0, 2).toUpperCase()}</AvatarFallback>
81
- </Avatar>
82
- <div className="flex-1 min-w-0 text-left">
83
- <p className="text-sm font-medium truncate">{user.name}</p>
84
- <p className="text-xs text-muted-foreground truncate">{user.email}</p>
85
- </div>
86
- <ChevronUp className="h-4 w-4 text-muted-foreground" />
87
- </button>
88
- </DropdownMenuTrigger>
89
- <DropdownMenuContent align="end" className="w-56">
90
- <DropdownMenuItem asChild>
91
- <Link href="/settings" onClick={handleLinkClick}>
92
- <Settings className="mr-2 h-4 w-4" />
93
- 设置
94
- </Link>
95
- </DropdownMenuItem>
96
- <DropdownMenuSeparator />
97
- <DropdownMenuItem onClick={handleLogout}>
98
- <LogOut className="mr-2 h-4 w-4" />
99
- 退出登录
100
- </DropdownMenuItem>
101
- </DropdownMenuContent>
102
- </DropdownMenu>
103
- </div>
104
- ```
105
-
106
- **Step 3: Remove unused imports**
107
-
108
- Remove `Badge` and `Building2` from imports (no longer used).
109
-
110
- **Step 4: Remove unused code**
111
-
112
- Remove `roleLabels` object and `isAdmin` variable if no longer used.
113
-
114
- **Step 5: Run build to verify**
115
-
116
- Run: `bun run build 2>&1 | tail -10`
117
- Expected: Build succeeds
118
-
119
- **Step 6: Commit**
120
-
121
- ```bash
122
- git add components/layout/sidebar.tsx
123
- git commit -m "feat: add user dropdown menu with settings and logout"
124
- ```
125
-
126
- ---
127
-
128
- ## Task 3: Create Settings Layout with Sidebar
129
-
130
- **Files:**
131
- - Create: `app/(dashboard)/settings/layout.tsx`
132
- - Create: `components/settings/settings-sidebar.tsx`
133
- - Create: `components/settings/index.ts`
134
-
135
- **Step 1: Create settings sidebar component**
136
-
137
- Create `components/settings/settings-sidebar.tsx`:
138
-
139
- ```tsx
140
- "use client";
141
-
142
- import Link from "next/link";
143
- import { usePathname } from "next/navigation";
144
- import { cn } from "@/lib/utils";
145
- import { User, Bell, Palette, Building2, Key } from "lucide-react";
146
- import type { UserRole } from "@/lib/auth/permissions";
147
-
148
- interface SettingsSidebarProps {
149
- userRole: UserRole;
150
- }
151
-
152
- export function SettingsSidebar({ userRole }: SettingsSidebarProps) {
153
- const pathname = usePathname();
154
- const isAdmin = userRole === "admin";
155
- const isAdminOrPM = isAdmin || userRole === "product_manager";
156
-
157
- const menuItems = [
158
- { href: "/settings/profile", label: "个人资料", icon: User },
159
- { href: "/settings/notifications", label: "通知设置", icon: Bell },
160
- { href: "/settings/appearance", label: "外观设置", icon: Palette },
161
- ];
162
-
163
- const adminItems = [
164
- { href: "/settings/organization", label: "组织管理", icon: Building2, show: isAdmin },
165
- { href: "/settings/api-keys", label: "API 密钥", icon: Key, show: isAdminOrPM },
166
- ];
167
-
168
- return (
169
- <nav className="w-56 shrink-0 border-r bg-muted/30 p-4">
170
- <h2 className="mb-4 px-2 text-lg font-semibold">设置</h2>
171
- <div className="space-y-1">
172
- {menuItems.map((item) => (
173
- <Link
174
- key={item.href}
175
- href={item.href}
176
- className={cn(
177
- "flex items-center gap-3 rounded-lg px-3 py-2 text-sm transition-colors hover:bg-muted",
178
- pathname === item.href && "bg-muted font-medium"
179
- )}
180
- >
181
- <item.icon className="h-4 w-4" />
182
- {item.label}
183
- </Link>
184
- ))}
185
- {adminItems.filter(item => item.show).map((item) => (
186
- <Link
187
- key={item.href}
188
- href={item.href}
189
- className={cn(
190
- "flex items-center gap-3 rounded-lg px-3 py-2 text-sm transition-colors hover:bg-muted",
191
- pathname === item.href && "bg-muted font-medium"
192
- )}
193
- >
194
- <item.icon className="h-4 w-4" />
195
- {item.label}
196
- </Link>
197
- ))}
198
- </div>
199
- </nav>
200
- );
201
- }
202
- ```
203
-
204
- **Step 2: Create settings index export**
205
-
206
- Create `components/settings/index.ts`:
207
-
208
- ```ts
209
- export { SettingsSidebar } from "./settings-sidebar";
210
- ```
211
-
212
- **Step 3: Create settings layout**
213
-
214
- Create `app/(dashboard)/settings/layout.tsx`:
215
-
216
- ```tsx
217
- import { headers } from "next/headers";
218
- import { redirect } from "next/navigation";
219
- import { auth } from "@/lib/auth/config";
220
- import { SettingsSidebar } from "@/components/settings";
221
- import type { UserRole } from "@/lib/auth/permissions";
222
-
223
- export default async function SettingsLayout({
224
- children,
225
- }: {
226
- children: React.ReactNode;
227
- }) {
228
- const session = await auth.api.getSession({ headers: await headers() });
229
-
230
- if (!session?.user) {
231
- redirect("/login");
232
- }
233
-
234
- const userRole = (session.user as { role?: string }).role as UserRole || "customer";
235
-
236
- return (
237
- <div className="flex min-h-[calc(100vh-3.5rem)]">
238
- <SettingsSidebar userRole={userRole} />
239
- <main className="flex-1 p-6">{children}</main>
240
- </div>
241
- );
242
- }
243
- ```
244
-
245
- **Step 4: Run build to verify**
246
-
247
- Run: `bun run build 2>&1 | tail -10`
248
- Expected: Build succeeds
249
-
250
- **Step 5: Commit**
251
-
252
- ```bash
253
- git add components/settings/settings-sidebar.tsx components/settings/index.ts app/\(dashboard\)/settings/layout.tsx
254
- git commit -m "feat: add settings layout with sidebar navigation"
255
- ```
256
-
257
- ---
258
-
259
- ## Task 4: Create Settings Root Page (Redirect)
260
-
261
- **Files:**
262
- - Create: `app/(dashboard)/settings/page.tsx`
263
-
264
- **Step 1: Create redirect page**
265
-
266
- Create `app/(dashboard)/settings/page.tsx`:
267
-
268
- ```tsx
269
- import { redirect } from "next/navigation";
270
-
271
- export default function SettingsPage() {
272
- redirect("/settings/profile");
273
- }
274
- ```
275
-
276
- **Step 2: Run build to verify**
277
-
278
- Run: `bun run build 2>&1 | tail -10`
279
- Expected: Build succeeds
280
-
281
- **Step 3: Commit**
282
-
283
- ```bash
284
- git add app/\(dashboard\)/settings/page.tsx
285
- git commit -m "feat: add settings root page with redirect to profile"
286
- ```
287
-
288
- ---
289
-
290
- ## Task 5: Create Profile Settings Page
291
-
292
- **Files:**
293
- - Create: `app/(dashboard)/settings/profile/page.tsx`
294
- - Create: `components/settings/profile-form.tsx`
295
-
296
- **Step 1: Create profile form component**
297
-
298
- Create `components/settings/profile-form.tsx`:
299
-
300
- ```tsx
301
- "use client";
302
-
303
- import { useState } from "react";
304
- import { Button } from "@/components/ui/button";
305
- import { Input } from "@/components/ui/input";
306
- import { Label } from "@/components/ui/label";
307
- import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
308
- import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
309
-
310
- interface ProfileFormProps {
311
- user: {
312
- id: string;
313
- name: string;
314
- email: string;
315
- image?: string | null;
316
- };
317
- }
318
-
319
- export function ProfileForm({ user }: ProfileFormProps) {
320
- const [name, setName] = useState(user.name);
321
- const [isLoading, setIsLoading] = useState(false);
322
-
323
- const handleSubmit = async (e: React.FormEvent) => {
324
- e.preventDefault();
325
- setIsLoading(true);
326
- // TODO: Implement profile update API
327
- setTimeout(() => setIsLoading(false), 1000);
328
- };
329
-
330
- return (
331
- <div className="space-y-6">
332
- <Card>
333
- <CardHeader>
334
- <CardTitle>个人资料</CardTitle>
335
- <CardDescription>管理您的账户信息</CardDescription>
336
- </CardHeader>
337
- <CardContent>
338
- <form onSubmit={handleSubmit} className="space-y-6">
339
- <div className="flex items-center gap-4">
340
- <Avatar className="h-20 w-20">
341
- <AvatarImage src={user.image || undefined} alt={user.name} />
342
- <AvatarFallback className="text-lg">
343
- {user.name.slice(0, 2).toUpperCase()}
344
- </AvatarFallback>
345
- </Avatar>
346
- <Button type="button" variant="outline" disabled>
347
- 更换头像
348
- </Button>
349
- </div>
350
-
351
- <div className="space-y-2">
352
- <Label htmlFor="name">姓名</Label>
353
- <Input
354
- id="name"
355
- value={name}
356
- onChange={(e) => setName(e.target.value)}
357
- placeholder="您的姓名"
358
- />
359
- </div>
360
-
361
- <div className="space-y-2">
362
- <Label htmlFor="email">邮箱</Label>
363
- <Input
364
- id="email"
365
- value={user.email}
366
- disabled
367
- className="bg-muted"
368
- />
369
- <p className="text-xs text-muted-foreground">邮箱地址不可修改</p>
370
- </div>
371
-
372
- <Button type="submit" disabled={isLoading}>
373
- {isLoading ? "保存中..." : "保存更改"}
374
- </Button>
375
- </form>
376
- </CardContent>
377
- </Card>
378
-
379
- <Card>
380
- <CardHeader>
381
- <CardTitle>修改密码</CardTitle>
382
- <CardDescription>更新您的登录密码</CardDescription>
383
- </CardHeader>
384
- <CardContent>
385
- <Button variant="outline" disabled>
386
- 修改密码
387
- </Button>
388
- <p className="mt-2 text-xs text-muted-foreground">密码修改功能即将推出</p>
389
- </CardContent>
390
- </Card>
391
- </div>
392
- );
393
- }
394
- ```
395
-
396
- **Step 2: Create profile page**
397
-
398
- Create `app/(dashboard)/settings/profile/page.tsx`:
399
-
400
- ```tsx
401
- import { headers } from "next/headers";
402
- import { redirect } from "next/navigation";
403
- import { auth } from "@/lib/auth/config";
404
- import { ProfileForm } from "@/components/settings/profile-form";
405
-
406
- export const metadata = {
407
- title: "个人资料 - Echo",
408
- };
409
-
410
- export default async function ProfileSettingsPage() {
411
- const session = await auth.api.getSession({ headers: await headers() });
412
-
413
- if (!session?.user) {
414
- redirect("/login");
415
- }
416
-
417
- return (
418
- <div className="max-w-2xl">
419
- <div className="mb-6">
420
- <h1 className="text-2xl font-semibold">个人资料</h1>
421
- <p className="text-muted-foreground">管理您的账户信息和偏好设置</p>
422
- </div>
423
- <ProfileForm user={session.user} />
424
- </div>
425
- );
426
- }
427
- ```
428
-
429
- **Step 3: Update settings index export**
430
-
431
- Update `components/settings/index.ts`:
432
-
433
- ```ts
434
- export { SettingsSidebar } from "./settings-sidebar";
435
- export { ProfileForm } from "./profile-form";
436
- ```
437
-
438
- **Step 4: Run build to verify**
439
-
440
- Run: `bun run build 2>&1 | tail -10`
441
- Expected: Build succeeds
442
-
443
- **Step 5: Commit**
444
-
445
- ```bash
446
- git add components/settings/profile-form.tsx components/settings/index.ts app/\(dashboard\)/settings/profile/page.tsx
447
- git commit -m "feat: add profile settings page"
448
- ```
449
-
450
- ---
451
-
452
- ## Task 6: Update Notifications Page to Use New Layout
453
-
454
- **Files:**
455
- - Modify: `app/(dashboard)/settings/notifications/page.tsx`
456
-
457
- **Step 1: Update notifications page**
458
-
459
- Replace `app/(dashboard)/settings/notifications/page.tsx`:
460
-
461
- ```tsx
462
- import { headers } from "next/headers";
463
- import { redirect } from "next/navigation";
464
- import { auth } from "@/lib/auth/config";
465
- import { NotificationPreferences } from "@/components/settings/notification-preferences";
466
-
467
- export const metadata = {
468
- title: "通知设置 - Echo",
469
- };
470
-
471
- export default async function NotificationSettingsPage() {
472
- const session = await auth.api.getSession({ headers: await headers() });
473
-
474
- if (!session?.user) {
475
- redirect("/login");
476
- }
477
-
478
- return (
479
- <div className="max-w-2xl">
480
- <div className="mb-6">
481
- <h1 className="text-2xl font-semibold">通知设置</h1>
482
- <p className="text-muted-foreground">选择您希望接收的通知类型</p>
483
- </div>
484
- <NotificationPreferences />
485
- </div>
486
- );
487
- }
488
- ```
489
-
490
- **Step 2: Run build to verify**
491
-
492
- Run: `bun run build 2>&1 | tail -10`
493
- Expected: Build succeeds
494
-
495
- **Step 3: Commit**
496
-
497
- ```bash
498
- git add app/\(dashboard\)/settings/notifications/page.tsx
499
- git commit -m "refactor: update notifications page to use settings layout"
500
- ```
501
-
502
- ---
503
-
504
- ## Task 7: Create Appearance Settings Page
505
-
506
- **Files:**
507
- - Create: `app/(dashboard)/settings/appearance/page.tsx`
508
- - Create: `components/settings/appearance-form.tsx`
509
-
510
- **Step 1: Create appearance form component**
511
-
512
- Create `components/settings/appearance-form.tsx`:
513
-
514
- ```tsx
515
- "use client";
516
-
517
- import { useState } from "react";
518
- import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
519
- import { Label } from "@/components/ui/label";
520
- import { cn } from "@/lib/utils";
521
- import { Monitor, Moon, Sun } from "lucide-react";
522
-
523
- type Theme = "light" | "dark" | "system";
524
-
525
- export function AppearanceForm() {
526
- const [theme, setTheme] = useState<Theme>("system");
527
-
528
- const themes: { value: Theme; label: string; icon: React.ElementType }[] = [
529
- { value: "light", label: "浅色", icon: Sun },
530
- { value: "dark", label: "深色", icon: Moon },
531
- { value: "system", label: "跟随系统", icon: Monitor },
532
- ];
533
-
534
- return (
535
- <Card>
536
- <CardHeader>
537
- <CardTitle>外观设置</CardTitle>
538
- <CardDescription>自定义应用的显示风格</CardDescription>
539
- </CardHeader>
540
- <CardContent>
541
- <div className="space-y-4">
542
- <Label>主题</Label>
543
- <div className="grid grid-cols-3 gap-4">
544
- {themes.map((t) => (
545
- <button
546
- key={t.value}
547
- onClick={() => setTheme(t.value)}
548
- className={cn(
549
- "flex flex-col items-center gap-2 rounded-lg border-2 p-4 transition-colors hover:bg-muted",
550
- theme === t.value ? "border-primary" : "border-transparent"
551
- )}
552
- >
553
- <t.icon className="h-6 w-6" />
554
- <span className="text-sm font-medium">{t.label}</span>
555
- </button>
556
- ))}
557
- </div>
558
- <p className="text-xs text-muted-foreground">
559
- 主题切换功能即将推出
560
- </p>
561
- </div>
562
- </CardContent>
563
- </Card>
564
- );
565
- }
566
- ```
567
-
568
- **Step 2: Create appearance page**
569
-
570
- Create `app/(dashboard)/settings/appearance/page.tsx`:
571
-
572
- ```tsx
573
- import { AppearanceForm } from "@/components/settings/appearance-form";
574
-
575
- export const metadata = {
576
- title: "外观设置 - Echo",
577
- };
578
-
579
- export default function AppearanceSettingsPage() {
580
- return (
581
- <div className="max-w-2xl">
582
- <div className="mb-6">
583
- <h1 className="text-2xl font-semibold">外观设置</h1>
584
- <p className="text-muted-foreground">自定义应用的显示风格</p>
585
- </div>
586
- <AppearanceForm />
587
- </div>
588
- );
589
- }
590
- ```
591
-
592
- **Step 3: Update settings index export**
593
-
594
- Update `components/settings/index.ts`:
595
-
596
- ```ts
597
- export { SettingsSidebar } from "./settings-sidebar";
598
- export { ProfileForm } from "./profile-form";
599
- export { AppearanceForm } from "./appearance-form";
600
- ```
601
-
602
- **Step 4: Run build to verify**
603
-
604
- Run: `bun run build 2>&1 | tail -10`
605
- Expected: Build succeeds
606
-
607
- **Step 5: Commit**
608
-
609
- ```bash
610
- git add components/settings/appearance-form.tsx components/settings/index.ts app/\(dashboard\)/settings/appearance/page.tsx
611
- git commit -m "feat: add appearance settings page"
612
- ```
613
-
614
- ---
615
-
616
- ## Task 8: Create Organization Settings Page
617
-
618
- **Files:**
619
- - Create: `app/(dashboard)/settings/organization/page.tsx`
620
-
621
- **Step 1: Create organization settings page**
622
-
623
- Create `app/(dashboard)/settings/organization/page.tsx`:
624
-
625
- ```tsx
626
- import { headers } from "next/headers";
627
- import { redirect } from "next/navigation";
628
- import { auth } from "@/lib/auth/config";
629
- import { db } from "@/lib/db";
630
- import { getUserOrganization } from "@/lib/auth/organization";
631
- import { OrganizationForm } from "@/components/settings/organization-form";
632
- import { OrganizationMembersList } from "@/components/settings/organization-members-list";
633
- import { InviteMemberForm } from "@/components/settings/invite-member-form";
634
- import type { UserRole } from "@/lib/auth/permissions";
635
-
636
- export const metadata = {
637
- title: "组织管理 - Echo",
638
- };
639
-
640
- export default async function OrganizationSettingsPage() {
641
- const session = await auth.api.getSession({ headers: await headers() });
642
-
643
- if (!session?.user) {
644
- redirect("/login");
645
- }
646
-
647
- const userRole = (session.user as { role?: string }).role as UserRole || "customer";
648
-
649
- if (userRole !== "admin") {
650
- redirect("/settings/profile");
651
- }
652
-
653
- let organization = null;
654
- if (db) {
655
- organization = await getUserOrganization(db, session.user.id);
656
- }
657
-
658
- if (!organization) {
659
- redirect("/settings/organizations/new");
660
- }
661
-
662
- return (
663
- <div className="max-w-4xl space-y-6">
664
- <div>
665
- <h1 className="text-2xl font-semibold">组织管理</h1>
666
- <p className="text-muted-foreground">管理您的组织信息和成员</p>
667
- </div>
668
-
669
- <OrganizationForm
670
- organizationId={organization.id}
671
- initialName={organization.name}
672
- initialSlug={organization.slug}
673
- />
674
-
675
- <InviteMemberForm organizationId={organization.id} />
676
-
677
- <OrganizationMembersList organizationId={organization.id} />
678
- </div>
679
- );
680
- }
681
- ```
682
-
683
- **Step 2: Run build to verify**
684
-
685
- Run: `bun run build 2>&1 | tail -10`
686
- Expected: Build succeeds
687
-
688
- **Step 3: Commit**
689
-
690
- ```bash
691
- git add app/\(dashboard\)/settings/organization/page.tsx
692
- git commit -m "feat: add organization settings page"
693
- ```
694
-
695
- ---
696
-
697
- ## Task 9: Create API Keys Settings Page
698
-
699
- **Files:**
700
- - Create: `app/(dashboard)/settings/api-keys/page.tsx`
701
- - Create: `components/settings/api-keys-list.tsx`
702
-
703
- **Step 1: Create API keys list component**
704
-
705
- Create `components/settings/api-keys-list.tsx`:
706
-
707
- ```tsx
708
- "use client";
709
-
710
- import { useState, useEffect } from "react";
711
- import { Button } from "@/components/ui/button";
712
- import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
713
- import { Input } from "@/components/ui/input";
714
- import { Label } from "@/components/ui/label";
715
- import { Key, Copy, Trash2, Plus } from "lucide-react";
716
-
717
- interface ApiKey {
718
- keyId: string;
719
- name: string;
720
- prefix: string;
721
- displayKey: string;
722
- createdAt: string;
723
- lastUsed: string | null;
724
- }
725
-
726
- export function ApiKeysList() {
727
- const [keys, setKeys] = useState<ApiKey[]>([]);
728
- const [isLoading, setIsLoading] = useState(true);
729
- const [newKeyName, setNewKeyName] = useState("");
730
- const [isCreating, setIsCreating] = useState(false);
731
-
732
- useEffect(() => {
733
- fetchKeys();
734
- }, []);
735
-
736
- const fetchKeys = async () => {
737
- try {
738
- const res = await fetch("/api/api-keys");
739
- if (res.ok) {
740
- const data = await res.json();
741
- setKeys(data.data || []);
742
- }
743
- } catch (error) {
744
- console.error("Failed to fetch API keys:", error);
745
- } finally {
746
- setIsLoading(false);
747
- }
748
- };
749
-
750
- const handleCreate = async (e: React.FormEvent) => {
751
- e.preventDefault();
752
- if (!newKeyName.trim()) return;
753
-
754
- setIsCreating(true);
755
- try {
756
- const res = await fetch("/api/api-keys", {
757
- method: "POST",
758
- headers: { "Content-Type": "application/json" },
759
- body: JSON.stringify({ name: newKeyName }),
760
- });
761
-
762
- if (res.ok) {
763
- const data = await res.json();
764
- alert(`新密钥已创建: ${data.data.key}\n\n请立即保存此密钥,它将不会再次显示。`);
765
- setNewKeyName("");
766
- fetchKeys();
767
- }
768
- } catch (error) {
769
- console.error("Failed to create API key:", error);
770
- } finally {
771
- setIsCreating(false);
772
- }
773
- };
774
-
775
- const handleDelete = async (keyId: string) => {
776
- if (!confirm("确定要删除此 API 密钥吗?此操作不可撤销。")) return;
777
-
778
- try {
779
- const res = await fetch(`/api/api-keys/${keyId}`, { method: "DELETE" });
780
- if (res.ok) {
781
- fetchKeys();
782
- }
783
- } catch (error) {
784
- console.error("Failed to delete API key:", error);
785
- }
786
- };
787
-
788
- if (isLoading) {
789
- return <div className="text-muted-foreground">加载中...</div>;
790
- }
791
-
792
- return (
793
- <div className="space-y-6">
794
- <Card>
795
- <CardHeader>
796
- <CardTitle>创建新密钥</CardTitle>
797
- <CardDescription>创建新的 API 密钥用于外部集成</CardDescription>
798
- </CardHeader>
799
- <CardContent>
800
- <form onSubmit={handleCreate} className="flex gap-4">
801
- <div className="flex-1">
802
- <Label htmlFor="keyName" className="sr-only">密钥名称</Label>
803
- <Input
804
- id="keyName"
805
- value={newKeyName}
806
- onChange={(e) => setNewKeyName(e.target.value)}
807
- placeholder="输入密钥名称"
808
- />
809
- </div>
810
- <Button type="submit" disabled={isCreating || !newKeyName.trim()}>
811
- <Plus className="mr-2 h-4 w-4" />
812
- 创建密钥
813
- </Button>
814
- </form>
815
- </CardContent>
816
- </Card>
817
-
818
- <Card>
819
- <CardHeader>
820
- <CardTitle>已有密钥</CardTitle>
821
- <CardDescription>管理您的 API 密钥</CardDescription>
822
- </CardHeader>
823
- <CardContent>
824
- {keys.length === 0 ? (
825
- <p className="text-muted-foreground py-4">暂无 API 密钥</p>
826
- ) : (
827
- <div className="space-y-4">
828
- {keys.map((key) => (
829
- <div key={key.keyId} className="flex items-center justify-between p-4 border rounded-lg">
830
- <div className="flex items-center gap-3">
831
- <Key className="h-5 w-5 text-muted-foreground" />
832
- <div>
833
- <p className="font-medium">{key.name}</p>
834
- <p className="text-sm text-muted-foreground font-mono">{key.displayKey}</p>
835
- </div>
836
- </div>
837
- <div className="flex items-center gap-2">
838
- <Button variant="ghost" size="icon" onClick={() => navigator.clipboard.writeText(key.prefix)}>
839
- <Copy className="h-4 w-4" />
840
- </Button>
841
- <Button variant="ghost" size="icon" onClick={() => handleDelete(key.keyId)}>
842
- <Trash2 className="h-4 w-4 text-destructive" />
843
- </Button>
844
- </div>
845
- </div>
846
- ))}
847
- </div>
848
- )}
849
- </CardContent>
850
- </Card>
851
- </div>
852
- );
853
- }
854
- ```
855
-
856
- **Step 2: Create API keys page**
857
-
858
- Create `app/(dashboard)/settings/api-keys/page.tsx`:
859
-
860
- ```tsx
861
- import { headers } from "next/headers";
862
- import { redirect } from "next/navigation";
863
- import { auth } from "@/lib/auth/config";
864
- import { ApiKeysList } from "@/components/settings/api-keys-list";
865
- import type { UserRole } from "@/lib/auth/permissions";
866
-
867
- export const metadata = {
868
- title: "API 密钥 - Echo",
869
- };
870
-
871
- export default async function ApiKeysSettingsPage() {
872
- const session = await auth.api.getSession({ headers: await headers() });
873
-
874
- if (!session?.user) {
875
- redirect("/login");
876
- }
877
-
878
- const userRole = (session.user as { role?: string }).role as UserRole || "customer";
879
-
880
- if (userRole !== "admin" && userRole !== "product_manager") {
881
- redirect("/settings/profile");
882
- }
883
-
884
- return (
885
- <div className="max-w-2xl">
886
- <div className="mb-6">
887
- <h1 className="text-2xl font-semibold">API 密钥</h1>
888
- <p className="text-muted-foreground">管理您的 API 访问密钥</p>
889
- </div>
890
- <ApiKeysList />
891
- </div>
892
- );
893
- }
894
- ```
895
-
896
- **Step 3: Update settings index export**
897
-
898
- Update `components/settings/index.ts`:
899
-
900
- ```ts
901
- export { SettingsSidebar } from "./settings-sidebar";
902
- export { ProfileForm } from "./profile-form";
903
- export { AppearanceForm } from "./appearance-form";
904
- export { ApiKeysList } from "./api-keys-list";
905
- ```
906
-
907
- **Step 4: Run build to verify**
908
-
909
- Run: `bun run build 2>&1 | tail -10`
910
- Expected: Build succeeds
911
-
912
- **Step 5: Commit**
913
-
914
- ```bash
915
- git add components/settings/api-keys-list.tsx components/settings/index.ts app/\(dashboard\)/settings/api-keys/page.tsx
916
- git commit -m "feat: add API keys settings page"
917
- ```
918
-
919
- ---
920
-
921
- ## Task 10: Final Verification and Cleanup
922
-
923
- **Step 1: Run full build**
924
-
925
- Run: `bun run build`
926
- Expected: Build succeeds with no errors
927
-
928
- **Step 2: Run linting**
929
-
930
- Run: `bun run lint`
931
- Expected: No errors (warnings acceptable)
932
-
933
- **Step 3: Test manually**
934
-
935
- 1. Start dev server: `bun dev`
936
- 2. Login as admin user
937
- 3. Verify user dropdown appears in sidebar
938
- 4. Click "设置" → redirects to /settings/profile
939
- 5. Verify left sidebar shows all menu items
940
- 6. Navigate through each settings page
941
- 7. Verify non-admin users cannot access organization/api-keys pages
942
-
943
- **Step 4: Final commit if any fixes needed**
944
-
945
- ```bash
946
- git add -A
947
- git commit -m "chore: cleanup and final adjustments"
948
- ```