@nexttylabs/echo 0.2.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 (579) hide show
  1. package/.changeset/README.md +21 -0
  2. package/.changeset/config.json +11 -0
  3. package/.changeset/cozy-ghosts-care.md +5 -0
  4. package/.changeset/sharp-lines-stand.md +5 -0
  5. package/.changeset/sour-doodles-eat.md +5 -0
  6. package/.changeset/tender-moose-shop.md +5 -0
  7. package/.github/pull_request_template.md +13 -0
  8. package/.github/workflows/ci.yml +41 -0
  9. package/.github/workflows/publish.yml +44 -0
  10. package/.github/workflows/release.yml +73 -0
  11. package/AGENTS.md +92 -0
  12. package/CHANGELOG.md +13 -0
  13. package/Dockerfile +57 -0
  14. package/LICENSE +661 -0
  15. package/Makefile +77 -0
  16. package/README.md +198 -0
  17. package/app/(auth)/login/page.tsx +53 -0
  18. package/app/(auth)/register/page.tsx +48 -0
  19. package/app/(auth)/sign-in/page.tsx +22 -0
  20. package/app/(dashboard)/admin/feedback/[id]/edit/page.tsx +103 -0
  21. package/app/(dashboard)/admin/feedback/[id]/page.tsx +154 -0
  22. package/app/(dashboard)/admin/feedback/new/page.tsx +91 -0
  23. package/app/(dashboard)/admin/feedback/page.tsx +81 -0
  24. package/app/(dashboard)/admin/layout.tsx +48 -0
  25. package/app/(dashboard)/analytics/portal/page.tsx +30 -0
  26. package/app/(dashboard)/dashboard/page.tsx +133 -0
  27. package/app/(dashboard)/layout.tsx +69 -0
  28. package/app/(dashboard)/no-access/page.tsx +45 -0
  29. package/app/(dashboard)/settings/access/page.tsx +56 -0
  30. package/app/(dashboard)/settings/api-keys/page.tsx +55 -0
  31. package/app/(dashboard)/settings/appearance/page.tsx +40 -0
  32. package/app/(dashboard)/settings/branding/page.tsx +62 -0
  33. package/app/(dashboard)/settings/changelog/page.tsx +51 -0
  34. package/app/(dashboard)/settings/danger-zone/page.tsx +92 -0
  35. package/app/(dashboard)/settings/feedback/page.tsx +63 -0
  36. package/app/(dashboard)/settings/integrations/page.tsx +94 -0
  37. package/app/(dashboard)/settings/layout.tsx +43 -0
  38. package/app/(dashboard)/settings/modules/page.tsx +54 -0
  39. package/app/(dashboard)/settings/notifications/page.tsx +48 -0
  40. package/app/(dashboard)/settings/organization/page.tsx +104 -0
  41. package/app/(dashboard)/settings/organization/portal/access/page.tsx +22 -0
  42. package/app/(dashboard)/settings/organization/portal/experience/page.tsx +22 -0
  43. package/app/(dashboard)/settings/organization/portal/growth/page.tsx +22 -0
  44. package/app/(dashboard)/settings/organization/portal/layout.tsx +24 -0
  45. package/app/(dashboard)/settings/organization/portal/page.tsx +22 -0
  46. package/app/(dashboard)/settings/organizations/[orgId]/members/page.tsx +69 -0
  47. package/app/(dashboard)/settings/organizations/new/page.tsx +36 -0
  48. package/app/(dashboard)/settings/page.tsx +22 -0
  49. package/app/(dashboard)/settings/portal-access/page.tsx +53 -0
  50. package/app/(dashboard)/settings/portal-branding/page.tsx +59 -0
  51. package/app/(dashboard)/settings/portal-growth/page.tsx +57 -0
  52. package/app/(dashboard)/settings/portal-modules/page.tsx +49 -0
  53. package/app/(dashboard)/settings/portal-resources/page.tsx +66 -0
  54. package/app/(dashboard)/settings/profile/page.tsx +48 -0
  55. package/app/(dashboard)/settings/widgets/page.tsx +63 -0
  56. package/app/(public)/[organizationSlug]/changelog/page.tsx +109 -0
  57. package/app/(public)/[organizationSlug]/feedback/[id]/page.tsx +146 -0
  58. package/app/(public)/[organizationSlug]/page.tsx +160 -0
  59. package/app/(public)/[organizationSlug]/roadmap/page.tsx +142 -0
  60. package/app/(public)/docs/page.tsx +48 -0
  61. package/app/(public)/feedback/[id]/not-found.tsx +33 -0
  62. package/app/(public)/feedback/[id]/page.tsx +102 -0
  63. package/app/(public)/invite/[token]/page.tsx +121 -0
  64. package/app/(public)/page.tsx +22 -0
  65. package/app/(public)/widget/[organizationId]/page.tsx +122 -0
  66. package/app/api/_utils.ts +29 -0
  67. package/app/api/admin/backup/route.ts +72 -0
  68. package/app/api/api-keys/[keyId]/route.ts +92 -0
  69. package/app/api/api-keys/route.ts +116 -0
  70. package/app/api/auth/[...all]/route.ts +21 -0
  71. package/app/api/auth/clear-session/route.ts +43 -0
  72. package/app/api/auth/register/handler.ts +176 -0
  73. package/app/api/auth/register/route.ts +26 -0
  74. package/app/api/docs/route.ts +28 -0
  75. package/app/api/feedback/[id]/comments/[commentId]/route.ts +105 -0
  76. package/app/api/feedback/[id]/comments/route.ts +421 -0
  77. package/app/api/feedback/[id]/duplicates/route.ts +285 -0
  78. package/app/api/feedback/[id]/handler.ts +91 -0
  79. package/app/api/feedback/[id]/processing-status/route.ts +199 -0
  80. package/app/api/feedback/[id]/reclassify/route.ts +145 -0
  81. package/app/api/feedback/[id]/route.ts +511 -0
  82. package/app/api/feedback/[id]/suggest-tags/route.ts +227 -0
  83. package/app/api/feedback/[id]/sync-github/route.ts +52 -0
  84. package/app/api/feedback/[id]/vote/route.ts +431 -0
  85. package/app/api/feedback/bulk/route.ts +212 -0
  86. package/app/api/feedback/handler.ts +138 -0
  87. package/app/api/feedback/route.ts +298 -0
  88. package/app/api/feedback/similar/route.ts +100 -0
  89. package/app/api/health/route.test.ts +64 -0
  90. package/app/api/health/route.ts +92 -0
  91. package/app/api/identify/jwt/route.ts +29 -0
  92. package/app/api/integrations/github/route.ts +196 -0
  93. package/app/api/internal/domain-lookup/route.ts +67 -0
  94. package/app/api/invitations/accept/handler.ts +101 -0
  95. package/app/api/invitations/accept/route.ts +29 -0
  96. package/app/api/notifications/preferences/route.ts +109 -0
  97. package/app/api/organizations/[orgId]/handler.ts +123 -0
  98. package/app/api/organizations/[orgId]/invitations/handler.ts +121 -0
  99. package/app/api/organizations/[orgId]/invitations/route.ts +29 -0
  100. package/app/api/organizations/[orgId]/members/[memberId]/handler.ts +208 -0
  101. package/app/api/organizations/[orgId]/members/[memberId]/route.ts +30 -0
  102. package/app/api/organizations/[orgId]/members/handler.ts +77 -0
  103. package/app/api/organizations/[orgId]/members/route.ts +29 -0
  104. package/app/api/organizations/[orgId]/route.ts +30 -0
  105. package/app/api/organizations/handler.ts +97 -0
  106. package/app/api/organizations/route.ts +29 -0
  107. package/app/api/tags/sync/route.ts +88 -0
  108. package/app/api/upload/handler.ts +79 -0
  109. package/app/api/upload/route.ts +37 -0
  110. package/app/api/v1/feedback/[id]/route.ts +276 -0
  111. package/app/api/v1/feedback/route.ts +250 -0
  112. package/app/api/v1/spec/route.ts +356 -0
  113. package/app/api/webhooks/[webhookId]/route.ts +213 -0
  114. package/app/api/webhooks/github/route.ts +158 -0
  115. package/app/api/webhooks/route.ts +143 -0
  116. package/app/favicon.ico +0 -0
  117. package/app/globals.css +139 -0
  118. package/app/health/route.ts +108 -0
  119. package/app/layout.tsx +60 -0
  120. package/bun.lock +2503 -0
  121. package/components/api/rate-limit-info.tsx +86 -0
  122. package/components/api-keys/api-key-manager.tsx +262 -0
  123. package/components/auth/login-form.tsx +207 -0
  124. package/components/auth/register-form.tsx +230 -0
  125. package/components/comment/comment-form.tsx +111 -0
  126. package/components/comment/internal-notes.tsx +219 -0
  127. package/components/comment/public-comments.tsx +387 -0
  128. package/components/component-example-client-only.tsx +29 -0
  129. package/components/component-example.tsx +519 -0
  130. package/components/dashboard/index.ts +22 -0
  131. package/components/dashboard/organization-switcher.tsx +96 -0
  132. package/components/dashboard/quick-actions.tsx +57 -0
  133. package/components/dashboard/recent-feedback-list.tsx +152 -0
  134. package/components/dashboard/stats-cards.tsx +88 -0
  135. package/components/dashboard/status-chart.tsx +106 -0
  136. package/components/example.tsx +70 -0
  137. package/components/feedback/attachment-list.tsx +103 -0
  138. package/components/feedback/auto-classification-badge.tsx +92 -0
  139. package/components/feedback/classification-override.tsx +64 -0
  140. package/components/feedback/duplicate-suggestions-inline.tsx +158 -0
  141. package/components/feedback/duplicate-suggestions.tsx +188 -0
  142. package/components/feedback/embedded-feedback-form.tsx +439 -0
  143. package/components/feedback/feedback-actions.tsx +160 -0
  144. package/components/feedback/feedback-bulk-actions.tsx +184 -0
  145. package/components/feedback/feedback-detail-view.tsx +321 -0
  146. package/components/feedback/feedback-detail.tsx +305 -0
  147. package/components/feedback/feedback-edit-form.tsx +131 -0
  148. package/components/feedback/feedback-filters.tsx +222 -0
  149. package/components/feedback/feedback-list-controls.tsx +433 -0
  150. package/components/feedback/feedback-list-item.tsx +298 -0
  151. package/components/feedback/feedback-list-skeleton.tsx +49 -0
  152. package/components/feedback/feedback-list.tsx +523 -0
  153. package/components/feedback/feedback-sorter.tsx +117 -0
  154. package/components/feedback/feedback-stats.tsx +124 -0
  155. package/components/feedback/file-upload.tsx +289 -0
  156. package/components/feedback/processing-status.tsx +161 -0
  157. package/components/feedback/status-history.tsx +134 -0
  158. package/components/feedback/status-selector.tsx +153 -0
  159. package/components/feedback/submit-on-behalf-form.tsx +403 -0
  160. package/components/feedback/tag-suggestions.tsx +212 -0
  161. package/components/feedback/vote-button.tsx +113 -0
  162. package/components/feedback/vote-list.tsx +108 -0
  163. package/components/integrations/github-config.tsx +200 -0
  164. package/components/landing/hero.tsx +150 -0
  165. package/components/layout/dashboard-layout.tsx +59 -0
  166. package/components/layout/index.ts +20 -0
  167. package/components/layout/language-switcher.tsx +129 -0
  168. package/components/layout/mobile-sidebar.tsx +66 -0
  169. package/components/layout/sidebar.tsx +279 -0
  170. package/components/portal/changelog-entry.tsx +132 -0
  171. package/components/portal/changelog-list.tsx +85 -0
  172. package/components/portal/contributor-badge.tsx +29 -0
  173. package/components/portal/contributors-sidebar.tsx +98 -0
  174. package/components/portal/create-post-dialog.tsx +247 -0
  175. package/components/portal/feedback-board.tsx +205 -0
  176. package/components/portal/feedback-post-card.tsx +198 -0
  177. package/components/portal/help-center.tsx +169 -0
  178. package/components/portal/leaderboard.tsx +29 -0
  179. package/components/portal/portal-header.tsx +153 -0
  180. package/components/portal/portal-layout.tsx +62 -0
  181. package/components/portal/portal-modules-panel.tsx +118 -0
  182. package/components/portal/portal-nav.tsx +59 -0
  183. package/components/portal/portal-overview.tsx +174 -0
  184. package/components/portal/portal-settings-nav.tsx +62 -0
  185. package/components/portal/portal-settings-shell.tsx +71 -0
  186. package/components/portal/portal-shell.tsx +62 -0
  187. package/components/portal/portal-tab-nav.tsx +77 -0
  188. package/components/portal/project-switcher.tsx +20 -0
  189. package/components/portal/roadmap-board.tsx +82 -0
  190. package/components/portal/roadmap-card.tsx +76 -0
  191. package/components/portal/roadmap-column.tsx +78 -0
  192. package/components/portal/settings-forms/access-form.tsx +194 -0
  193. package/components/portal/settings-forms/copy-form.tsx +95 -0
  194. package/components/portal/settings-forms/index.ts +23 -0
  195. package/components/portal/settings-forms/languages-form.tsx +223 -0
  196. package/components/portal/settings-forms/seo-form.tsx +156 -0
  197. package/components/portal/settings-forms/sharing-form.tsx +155 -0
  198. package/components/portal/settings-forms/theme-form.tsx +104 -0
  199. package/components/settings/api-keys-list.tsx +167 -0
  200. package/components/settings/appearance-form.tsx +71 -0
  201. package/components/settings/index.ts +25 -0
  202. package/components/settings/invite-member-form.tsx +119 -0
  203. package/components/settings/notification-preferences.tsx +174 -0
  204. package/components/settings/organization-form.tsx +165 -0
  205. package/components/settings/organization-members-list.tsx +197 -0
  206. package/components/settings/profile-form.tsx +124 -0
  207. package/components/settings/role-selector.tsx +57 -0
  208. package/components/settings/settings-sidebar.tsx +115 -0
  209. package/components/shared/pagination.tsx +215 -0
  210. package/components/ui/alert-dialog.tsx +201 -0
  211. package/components/ui/alert.tsx +75 -0
  212. package/components/ui/avatar.tsx +126 -0
  213. package/components/ui/badge.tsx +62 -0
  214. package/components/ui/button.tsx +77 -0
  215. package/components/ui/card.tsx +111 -0
  216. package/components/ui/combobox.tsx +311 -0
  217. package/components/ui/dialog.tsx +158 -0
  218. package/components/ui/dropdown-menu.tsx +272 -0
  219. package/components/ui/field.tsx +256 -0
  220. package/components/ui/input-group.tsx +164 -0
  221. package/components/ui/input.tsx +36 -0
  222. package/components/ui/label.tsx +41 -0
  223. package/components/ui/pagination.tsx +142 -0
  224. package/components/ui/select.tsx +202 -0
  225. package/components/ui/separator.tsx +45 -0
  226. package/components/ui/sheet.tsx +151 -0
  227. package/components/ui/skeleton.tsx +32 -0
  228. package/components/ui/switch.tsx +49 -0
  229. package/components/ui/table.tsx +118 -0
  230. package/components/ui/tabs.tsx +107 -0
  231. package/components/ui/textarea.tsx +35 -0
  232. package/components/ui/tooltip.tsx +78 -0
  233. package/components/widget/widget-form.tsx +439 -0
  234. package/components.json +24 -0
  235. package/db/init/01-init.sql +13 -0
  236. package/docker-compose.dev.yml +26 -0
  237. package/docker-compose.yml +98 -0
  238. package/docs/architecture.md +259 -0
  239. package/docs/component-inventory.md +261 -0
  240. package/docs/database-migrations.md +76 -0
  241. package/docs/development-guide.md +209 -0
  242. package/docs/e2e-user-flows.csv +31 -0
  243. package/docs/er-diagram-feedback.mmd +138 -0
  244. package/docs/er-diagram.mmd +281 -0
  245. package/docs/i18n-check-report.md +296 -0
  246. package/docs/index.md +214 -0
  247. package/docs/logic-chain.md +94 -0
  248. package/docs/plans/2026-01-02-database-migration-scripts.md +496 -0
  249. package/docs/plans/2026-01-02-user-login-design.md +37 -0
  250. package/docs/plans/2026-01-02-user-login.md +437 -0
  251. package/docs/plans/2026-01-02-user-registration-design.md +47 -0
  252. package/docs/plans/2026-01-02-user-registration.md +628 -0
  253. package/docs/plans/2026-01-03-roles-permissions-design.md +20 -0
  254. package/docs/plans/2026-01-03-roles-permissions.md +266 -0
  255. package/docs/plans/2026-01-05-authentication-middleware.md +207 -0
  256. package/docs/plans/2026-01-05-member-removal.md +186 -0
  257. package/docs/plans/2026-01-05-organization-creation.md +374 -0
  258. package/docs/plans/2026-01-05-rbac-middleware.md +112 -0
  259. package/docs/plans/2026-01-05-role-configuration.md +441 -0
  260. package/docs/plans/2026-01-06-file-upload-support.md +804 -0
  261. package/docs/plans/2026-01-06-permission-check-hook.md +155 -0
  262. package/docs/plans/2026-01-06-resource-ownership-check.md +231 -0
  263. package/docs/plans/2026-01-07-feedback-tracking-link.md +459 -0
  264. package/docs/plans/2026-01-09-logout-redirect-design.md +52 -0
  265. package/docs/plans/2026-01-09-phase2-3-plan.md +654 -0
  266. package/docs/plans/2026-01-09-portal-execution-plan.md +408 -0
  267. package/docs/plans/2026-01-09-project-delete-feature-design.md +163 -0
  268. package/docs/plans/2026-01-09-project-delete-implementation.md +451 -0
  269. package/docs/plans/2026-01-09-project-edit-delete-design.md +52 -0
  270. package/docs/plans/2026-01-09-settings-center-design.md +114 -0
  271. package/docs/plans/2026-01-09-settings-center.md +948 -0
  272. package/docs/plans/2026-01-10-organization-only-design.md +66 -0
  273. package/docs/plans/2026-01-10-organization-only-implementation.md +433 -0
  274. package/docs/plans/2026-01-10-portal-settings-restructure-plan.md +18 -0
  275. package/docs/plans/2026-01-10-project-settings-tabs-design-implementation.md +296 -0
  276. package/docs/plans/2026-01-14-e2e-playwright-feedback.md +173 -0
  277. package/docs/plans/2026-01-15-feedback-management-org-context-design.md +82 -0
  278. package/docs/plans/2026-01-15-feedback-management-org-context-implementation-plan.md +521 -0
  279. package/docs/plans/2026-01-16-admin-feedback-filters-design.md +75 -0
  280. package/docs/plans/2026-01-16-admin-feedback-filters-implementation.md +293 -0
  281. package/docs/plans/2026-01-16-admin-feedback-route-consolidation.md +180 -0
  282. package/docs/plans/2026-01-16-e2e-test-fixes.md +158 -0
  283. package/docs/plans/2026-01-17-admin-feedback-filters.md +214 -0
  284. package/docs/plans/2026-01-17-admin-feedback-improvements.md +453 -0
  285. package/docs/plans/2026-01-18-changesets-design.md +40 -0
  286. package/docs/product_changes.md +37 -0
  287. package/docs/project-overview.md +159 -0
  288. package/docs/project-scan-report.json +104 -0
  289. package/docs/route-role-visibility.md +51 -0
  290. package/docs/source-tree-analysis.md +150 -0
  291. package/docs/testing/delete-project-manual-tests.md +18 -0
  292. package/docs/user-story-tracking.md +191 -0
  293. package/drizzle.config.ts +32 -0
  294. package/eslint.config.mjs +19 -0
  295. package/hooks/use-permissions.ts +56 -0
  296. package/i18n/config.ts +45 -0
  297. package/i18n/request.ts +28 -0
  298. package/i18n/resolve-locale.ts +38 -0
  299. package/lib/api/errors.ts +62 -0
  300. package/lib/auth/cli-config.ts +35 -0
  301. package/lib/auth/client.ts +20 -0
  302. package/lib/auth/config.ts +55 -0
  303. package/lib/auth/jwt-identity.ts +21 -0
  304. package/lib/auth/org-context.ts +71 -0
  305. package/lib/auth/organization.ts +107 -0
  306. package/lib/auth/permissions.ts +87 -0
  307. package/lib/auth/session.ts +23 -0
  308. package/lib/config/rate-limits.ts +64 -0
  309. package/lib/dashboard/get-dashboard-stats.ts +136 -0
  310. package/lib/db/index.ts +41 -0
  311. package/lib/db/migrate.test.ts +49 -0
  312. package/lib/db/migrate.ts +62 -0
  313. package/lib/db/migrations/.gitkeep +0 -0
  314. package/lib/db/migrations/0000_cynical_gladiator.sql +53 -0
  315. package/lib/db/migrations/0001_wandering_sunfire.sql +27 -0
  316. package/lib/db/migrations/0002_shallow_speedball.sql +1 -0
  317. package/lib/db/migrations/0003_add_org_description.sql +1 -0
  318. package/lib/db/migrations/0003_boring_wild_pack.sql +13 -0
  319. package/lib/db/migrations/0004_windy_tyrannus.sql +27 -0
  320. package/lib/db/migrations/0005_perpetual_doorman.sql +5 -0
  321. package/lib/db/migrations/0006_aberrant_captain_midlands.sql +13 -0
  322. package/lib/db/migrations/0007_clever_captain_cross.sql +14 -0
  323. package/lib/db/migrations/0008_sparkling_pandemic.sql +2 -0
  324. package/lib/db/migrations/0009_happy_black_tom.sql +29 -0
  325. package/lib/db/migrations/0010_kind_junta.sql +8 -0
  326. package/lib/db/migrations/0011_mute_squadron_supreme.sql +25 -0
  327. package/lib/db/migrations/0012_giant_power_man.sql +24 -0
  328. package/lib/db/migrations/0013_damp_titanium_man.sql +17 -0
  329. package/lib/db/migrations/0014_blue_alice.sql +18 -0
  330. package/lib/db/migrations/0015_webhook_tables.sql +41 -0
  331. package/lib/db/migrations/0016_github_integration.sql +30 -0
  332. package/lib/db/migrations/0016_overjoyed_ghost_rider.sql +22 -0
  333. package/lib/db/migrations/0017_slimy_inhumans.sql +6 -0
  334. package/lib/db/migrations/0018_same_spitfire.sql +1 -0
  335. package/lib/db/migrations/0019_jittery_loners.sql +16 -0
  336. package/lib/db/migrations/0019_remove_projects_add_org_settings.sql +14 -0
  337. package/lib/db/migrations/meta/0000_snapshot.json +374 -0
  338. package/lib/db/migrations/meta/0001_snapshot.json +553 -0
  339. package/lib/db/migrations/meta/0002_snapshot.json +560 -0
  340. package/lib/db/migrations/meta/0003_snapshot.json +650 -0
  341. package/lib/db/migrations/meta/0004_snapshot.json +852 -0
  342. package/lib/db/migrations/meta/0005_snapshot.json +900 -0
  343. package/lib/db/migrations/meta/0006_snapshot.json +1011 -0
  344. package/lib/db/migrations/meta/0007_snapshot.json +1125 -0
  345. package/lib/db/migrations/meta/0008_snapshot.json +1146 -0
  346. package/lib/db/migrations/meta/0009_snapshot.json +1386 -0
  347. package/lib/db/migrations/meta/0010_snapshot.json +1419 -0
  348. package/lib/db/migrations/meta/0011_snapshot.json +1615 -0
  349. package/lib/db/migrations/meta/0012_snapshot.json +1805 -0
  350. package/lib/db/migrations/meta/0013_snapshot.json +1948 -0
  351. package/lib/db/migrations/meta/0014_snapshot.json +2082 -0
  352. package/lib/db/migrations/meta/0015_snapshot.json +2476 -0
  353. package/lib/db/migrations/meta/0016_snapshot.json +2633 -0
  354. package/lib/db/migrations/meta/0017_snapshot.json +2680 -0
  355. package/lib/db/migrations/meta/0018_snapshot.json +2686 -0
  356. package/lib/db/migrations/meta/0019_snapshot.json +2741 -0
  357. package/lib/db/migrations/meta/_journal.json +146 -0
  358. package/lib/db/schema/ai-processing.ts +90 -0
  359. package/lib/db/schema/api-keys.ts +61 -0
  360. package/lib/db/schema/attachments.ts +48 -0
  361. package/lib/db/schema/auth.ts +111 -0
  362. package/lib/db/schema/comments.ts +74 -0
  363. package/lib/db/schema/duplicates.ts +80 -0
  364. package/lib/db/schema/feedback.ts +88 -0
  365. package/lib/db/schema/github-integrations.ts +66 -0
  366. package/lib/db/schema/index.ts +35 -0
  367. package/lib/db/schema/invitations.ts +32 -0
  368. package/lib/db/schema/notifications.ts +85 -0
  369. package/lib/db/schema/organization-members.ts +37 -0
  370. package/lib/db/schema/organization-settings.ts +134 -0
  371. package/lib/db/schema/organizations.ts +30 -0
  372. package/lib/db/schema/projects.ts +145 -0
  373. package/lib/db/schema/status-history.ts +63 -0
  374. package/lib/db/schema/tags.ts +194 -0
  375. package/lib/db/schema/user-profiles.ts +31 -0
  376. package/lib/db/schema/votes.ts +60 -0
  377. package/lib/db/schema/webhooks.ts +106 -0
  378. package/lib/feedback/filters.ts +28 -0
  379. package/lib/feedback/find-similar.ts +49 -0
  380. package/lib/feedback/get-feedback-by-id.ts +159 -0
  381. package/lib/feedback/prefill.ts +51 -0
  382. package/lib/http/get-request-url.ts +28 -0
  383. package/lib/integrations/github.ts +159 -0
  384. package/lib/invitations.ts +22 -0
  385. package/lib/logger.test.ts +31 -0
  386. package/lib/logger.ts +58 -0
  387. package/lib/middleware/api-key.ts +126 -0
  388. package/lib/middleware/rate-limit-keys.ts +47 -0
  389. package/lib/middleware/rate-limit.ts +148 -0
  390. package/lib/middleware/rbac.ts +39 -0
  391. package/lib/middleware/request-id.test.ts +28 -0
  392. package/lib/middleware/request-id.ts +30 -0
  393. package/lib/middleware/request-logger.test.ts +36 -0
  394. package/lib/middleware/request-logger.ts +41 -0
  395. package/lib/middleware/with-rate-limit.ts +33 -0
  396. package/lib/portal/analytics.ts +20 -0
  397. package/lib/portal/contributors.ts +27 -0
  398. package/lib/portal/i18n.ts +20 -0
  399. package/lib/portal/leaderboard-settings.ts +20 -0
  400. package/lib/portal/modules.ts +20 -0
  401. package/lib/portal/portal-copy.ts +20 -0
  402. package/lib/portal/public-context.tsx +110 -0
  403. package/lib/portal/seo.ts +20 -0
  404. package/lib/portal/settings-context.ts +56 -0
  405. package/lib/portal/sharing.ts +20 -0
  406. package/lib/portal/sorting.ts +20 -0
  407. package/lib/portal/theme.ts +20 -0
  408. package/lib/services/ai/classifier.ts +296 -0
  409. package/lib/services/ai/duplicate-detector.ts +255 -0
  410. package/lib/services/ai/tag-suggester.ts +108 -0
  411. package/lib/services/api-keys.ts +164 -0
  412. package/lib/services/backup.ts +173 -0
  413. package/lib/services/email/templates.ts +158 -0
  414. package/lib/services/email.ts +68 -0
  415. package/lib/services/github-sync.ts +205 -0
  416. package/lib/services/notifications/index.ts +224 -0
  417. package/lib/services/portal-settings.ts +157 -0
  418. package/lib/swagger/config.ts +296 -0
  419. package/lib/swagger/generate.ts +400 -0
  420. package/lib/upload/file-validator.ts +52 -0
  421. package/lib/upload/storage.ts +59 -0
  422. package/lib/utils/format.ts +26 -0
  423. package/lib/utils/slug.ts +28 -0
  424. package/lib/utils.ts +23 -0
  425. package/lib/validations/auth.ts +56 -0
  426. package/lib/validations/comment.ts +44 -0
  427. package/lib/validations/feedback.ts +51 -0
  428. package/lib/validations/invitations.ts +23 -0
  429. package/lib/validations/organizations.ts +34 -0
  430. package/lib/validations/projects.ts +49 -0
  431. package/lib/validators/feedback.ts +57 -0
  432. package/lib/validators/index.ts +18 -0
  433. package/lib/webhooks/events.ts +73 -0
  434. package/lib/webhooks/index.ts +21 -0
  435. package/lib/webhooks/retry.ts +188 -0
  436. package/lib/webhooks/sender.ts +183 -0
  437. package/lib/webhooks/verify.ts +37 -0
  438. package/lib/workers/feedback-processor.ts +255 -0
  439. package/messages/en.json +965 -0
  440. package/messages/jp.json +862 -0
  441. package/messages/zh-CN.json +855 -0
  442. package/next-env.d.ts +6 -0
  443. package/next.config.ts +66 -0
  444. package/package.json +84 -0
  445. package/playwright.config.ts +44 -0
  446. package/postcss.config.mjs +7 -0
  447. package/proxy.test.ts +131 -0
  448. package/proxy.ts +190 -0
  449. package/public/file.svg +1 -0
  450. package/public/globe.svg +1 -0
  451. package/public/logo-64.svg +5 -0
  452. package/public/logo.svg +5 -0
  453. package/public/next.svg +1 -0
  454. package/public/openapi.json +673 -0
  455. package/public/uploads/.gitkeep +0 -0
  456. package/public/uploads/02695701-ded0-4c81-8a21-9326c1d65448.pdf +1 -0
  457. package/public/uploads/178843ea-2780-48ef-8988-f4cba442e4cb.pdf +1 -0
  458. package/public/uploads/24b0a9ef-da93-49da-934f-637f89c7871d.pdf +1 -0
  459. package/public/uploads/7a11626d-a8e4-4b91-a8eb-20b6213b0a5a.pdf +1 -0
  460. package/public/uploads/b0703f4d-6e7b-4aab-8191-1a7b15f1b8ee.pdf +1 -0
  461. package/public/uploads/c8de0aed-4d3a-44aa-83bb-6594b7a2ddb3.pdf +1 -0
  462. package/public/uploads/e4cce295-0d85-4525-a1b0-a61c45722e26.pdf +1 -0
  463. package/public/uploads/eb4df45e-563c-48b8-9c68-c18212312426.pdf +1 -0
  464. package/public/vercel.svg +1 -0
  465. package/public/widget/embed.js +249 -0
  466. package/public/window.svg +1 -0
  467. package/scripts/backup-db.sh +57 -0
  468. package/scripts/backup-db.ts +24 -0
  469. package/scripts/generate-openapi.ts +22 -0
  470. package/scripts/migration-helper.ts +39 -0
  471. package/scripts/pre-deploy.ts +75 -0
  472. package/scripts/restore-db.sh +60 -0
  473. package/scripts/rollback.ts +72 -0
  474. package/scripts/seed-tags.ts +48 -0
  475. package/tests/api/feedback-bulk.test.ts +47 -0
  476. package/tests/api/feedback-by-id.test.ts +67 -0
  477. package/tests/api/feedback-comments-route-import.test.ts +26 -0
  478. package/tests/api/feedback-create.test.ts +71 -0
  479. package/tests/api/feedback-delete.test.ts +160 -0
  480. package/tests/api/feedback-filter.test.ts +250 -0
  481. package/tests/api/feedback-list.test.ts +234 -0
  482. package/tests/api/feedback-route-assignee-condition.test.ts +32 -0
  483. package/tests/api/feedback-similar.test.ts +46 -0
  484. package/tests/api/feedback-sort.test.ts +261 -0
  485. package/tests/api/feedback-status-enum.test.ts +49 -0
  486. package/tests/api/feedback-status-filter.test.ts +117 -0
  487. package/tests/api/feedback-submit-on-behalf.test.ts +269 -0
  488. package/tests/api/feedback.test.ts +175 -0
  489. package/tests/api/identify-jwt.test.ts +25 -0
  490. package/tests/api/invitation-accept.test.ts +213 -0
  491. package/tests/api/organization-invitations.test.ts +186 -0
  492. package/tests/api/organization-members-list.test.ts +79 -0
  493. package/tests/api/organization-members.test.ts +340 -0
  494. package/tests/api/organizations.test.ts +149 -0
  495. package/tests/api/register.test.ts +112 -0
  496. package/tests/api/upload.test.ts +103 -0
  497. package/tests/api/vote.test.ts +82 -0
  498. package/tests/app/admin-feedback-detail-page.test.tsx +25 -0
  499. package/tests/app/admin-feedback-list-page.test.tsx +25 -0
  500. package/tests/app/admin-feedback-new-page.test.tsx +25 -0
  501. package/tests/app/health-route-helpers.test.ts +27 -0
  502. package/tests/app/login-page.test.ts +26 -0
  503. package/tests/app/portal-page.test.ts +29 -0
  504. package/tests/app/project-portal-overview.test.tsx +25 -0
  505. package/tests/app/widget-page-import.test.ts +25 -0
  506. package/tests/components/create-post-dialog-defaults.test.ts +43 -0
  507. package/tests/components/feedback/duplicate-suggestions-inline.test.tsx +27 -0
  508. package/tests/components/feedback/embedded-feedback-form.test.tsx +96 -0
  509. package/tests/components/feedback/feedback-detail.test.tsx +25 -0
  510. package/tests/components/feedback/feedback-stats.test.tsx +49 -0
  511. package/tests/components/feedback-bulk-actions.test.tsx +39 -0
  512. package/tests/components/feedback-i18n-keys.test.ts +70 -0
  513. package/tests/components/feedback-list-controls-compile.test.ts +25 -0
  514. package/tests/components/feedback-list-controls.test.tsx +204 -0
  515. package/tests/components/feedback-list-item.test.tsx +67 -0
  516. package/tests/components/landing/hero.test.tsx +46 -0
  517. package/tests/components/layout/language-switcher.test.tsx +25 -0
  518. package/tests/components/layout/sidebar.test.tsx +157 -0
  519. package/tests/components/login-form.test.ts +25 -0
  520. package/tests/components/organization-form.test.ts +32 -0
  521. package/tests/components/organization-switcher.test.ts +25 -0
  522. package/tests/components/pagination.test.tsx +43 -0
  523. package/tests/components/portal-overview.test.tsx +25 -0
  524. package/tests/components/profile-form.test.tsx +139 -0
  525. package/tests/components/role-selector.test.ts +31 -0
  526. package/tests/components/status-chart.test.tsx +90 -0
  527. package/tests/e2e/auth.e2e.ts +323 -0
  528. package/tests/e2e/feedback-actions.e2e.ts +471 -0
  529. package/tests/e2e/feedback-attachment.e2e.ts +168 -0
  530. package/tests/e2e/feedback-customer.e2e.ts +226 -0
  531. package/tests/e2e/feedback-management.e2e.ts +565 -0
  532. package/tests/e2e/feedback-submit.e2e.ts +133 -0
  533. package/tests/e2e/feedback-view.e2e.ts +297 -0
  534. package/tests/e2e/fixtures/test-data.ts +235 -0
  535. package/tests/e2e/health-check.e2e.ts +230 -0
  536. package/tests/e2e/helpers/test-utils-helpers.test.ts +43 -0
  537. package/tests/e2e/helpers/test-utils.ts +298 -0
  538. package/tests/e2e/integration-placeholders.e2e.ts +199 -0
  539. package/tests/e2e/organization.e2e.ts +292 -0
  540. package/tests/e2e/permissions.e2e.ts +424 -0
  541. package/tests/e2e/project-widget.e2e.ts +63 -0
  542. package/tests/feedback/filters.test.ts +29 -0
  543. package/tests/hooks/use-permissions.test.ts +52 -0
  544. package/tests/lib/ai/classifier.test.ts +104 -0
  545. package/tests/lib/ai/duplicate-detector.test.ts +234 -0
  546. package/tests/lib/attachments-schema.test.ts +30 -0
  547. package/tests/lib/auth/session.test.ts +49 -0
  548. package/tests/lib/auth-client.test.ts +37 -0
  549. package/tests/lib/auth-config.test.ts +26 -0
  550. package/tests/lib/feedback-prefill.test.ts +52 -0
  551. package/tests/lib/feedback-processor.test.ts +41 -0
  552. package/tests/lib/feedback-schema.test.ts +33 -0
  553. package/tests/lib/file-validator.test.ts +48 -0
  554. package/tests/lib/get-feedback-by-id.test.ts +37 -0
  555. package/tests/lib/invitations.test.ts +35 -0
  556. package/tests/lib/login-schema.test.ts +36 -0
  557. package/tests/lib/org-context.test.ts +95 -0
  558. package/tests/lib/organization-access.test.ts +44 -0
  559. package/tests/lib/organization-member-role-schema.test.ts +41 -0
  560. package/tests/lib/permissions.test.ts +88 -0
  561. package/tests/lib/portal-analytics.test.ts +25 -0
  562. package/tests/lib/portal-contributors.test.ts +25 -0
  563. package/tests/lib/portal-copy.test.ts +27 -0
  564. package/tests/lib/portal-i18n.test.ts +30 -0
  565. package/tests/lib/portal-leaderboard-settings.test.ts +25 -0
  566. package/tests/lib/portal-modules.test.ts +25 -0
  567. package/tests/lib/portal-seo.test.ts +25 -0
  568. package/tests/lib/portal-sharing.test.ts +25 -0
  569. package/tests/lib/portal-sorting.test.ts +25 -0
  570. package/tests/lib/portal-theme.test.ts +25 -0
  571. package/tests/lib/rate-limit.test.ts +142 -0
  572. package/tests/lib/resolve-locale.test.ts +34 -0
  573. package/tests/lib/services/backup.test.ts +145 -0
  574. package/tests/lib/user-organizations.test.ts +42 -0
  575. package/tests/lib/user-role-schema.test.ts +33 -0
  576. package/tests/lib/user-schema.test.ts +25 -0
  577. package/tests/setup.ts +74 -0
  578. package/tsconfig.json +34 -0
  579. package/types/bun-test.d.ts +31 -0
@@ -0,0 +1,78 @@
1
+ "use client";
2
+
3
+
4
+ /*
5
+ * Copyright (c) 2026 Echo Team
6
+ *
7
+ * This program is free software: you can redistribute it and/or modify
8
+ * it under the terms of the GNU Affero General Public License as published by
9
+ * the Free Software Foundation, either version 3 of the License, or
10
+ * (at your option) any later version.
11
+ *
12
+ * This program is distributed in the hope that it will be useful,
13
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
14
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15
+ * GNU Affero General Public License for more details.
16
+ *
17
+ * You should have received a copy of the GNU Affero General Public License
18
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
19
+ */
20
+
21
+ import { RoadmapCard } from "./roadmap-card";
22
+ import { cn } from "@/lib/utils";
23
+ import type { RoadmapStatus } from "./roadmap-board";
24
+
25
+ export interface RoadmapItem {
26
+ id: number;
27
+ title: string;
28
+ category?: string;
29
+ voteCount: number;
30
+ status: RoadmapStatus;
31
+ }
32
+
33
+ interface RoadmapColumnProps {
34
+ status: RoadmapStatus;
35
+ label: string;
36
+ items: RoadmapItem[];
37
+ headerColor?: string;
38
+ className?: string;
39
+ }
40
+
41
+ export function RoadmapColumn({
42
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
43
+ status: _status,
44
+ label,
45
+ items,
46
+ headerColor,
47
+ className,
48
+ }: RoadmapColumnProps) {
49
+ return (
50
+ <div className={cn("flex flex-col min-h-[400px] rounded-xl border bg-background", className)}>
51
+ {/* Column Header */}
52
+ <div
53
+ className={cn(
54
+ "flex items-center justify-between px-4 py-3 rounded-t-xl border-b",
55
+ headerColor
56
+ )}
57
+ >
58
+ <h3 className="font-semibold text-sm">{label}</h3>
59
+ <span className="text-xs text-muted-foreground bg-background/80 px-2 py-0.5 rounded-full">
60
+ {items.length}
61
+ </span>
62
+ </div>
63
+
64
+ {/* Column Content */}
65
+ <div className="flex-1 p-2 bg-muted/20 rounded-b-xl space-y-2">
66
+ {items.length === 0 ? (
67
+ <div className="flex items-center justify-center h-24 text-sm text-muted-foreground">
68
+ No items
69
+ </div>
70
+ ) : (
71
+ items.map((item) => (
72
+ <RoadmapCard key={item.id} item={item} />
73
+ ))
74
+ )}
75
+ </div>
76
+ </div>
77
+ );
78
+ }
@@ -0,0 +1,194 @@
1
+ "use client";
2
+
3
+
4
+ /*
5
+ * Copyright (c) 2026 Echo Team
6
+ *
7
+ * This program is free software: you can redistribute it and/or modify
8
+ * it under the terms of the GNU Affero General Public License as published by
9
+ * the Free Software Foundation, either version 3 of the License, or
10
+ * (at your option) any later version.
11
+ *
12
+ * This program is distributed in the hope that it will be useful,
13
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
14
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15
+ * GNU Affero General Public License for more details.
16
+ *
17
+ * You should have received a copy of the GNU Affero General Public License
18
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
19
+ */
20
+
21
+ import { useState } from "react";
22
+ import { useForm, useWatch } from "react-hook-form";
23
+ import { Button } from "@/components/ui/button";
24
+ import { Label } from "@/components/ui/label";
25
+ import { Switch } from "@/components/ui/switch";
26
+ import { Loader2 } from "lucide-react";
27
+ import { updatePortalSettings } from "@/lib/services/portal-settings";
28
+ import type { PortalSeoConfig, PortalSharingConfig } from "@/lib/db/schema";
29
+
30
+ interface PortalAccessFormProps {
31
+ organizationId: string;
32
+ initialSharing?: PortalSharingConfig;
33
+ initialSeo?: PortalSeoConfig;
34
+ }
35
+
36
+ type AccessFormData = {
37
+ enabled: boolean;
38
+ allowPublicVoting: boolean;
39
+ allowPublicComments: boolean;
40
+ showVoteCount: boolean;
41
+ showAuthor: boolean;
42
+ noIndex: boolean;
43
+ };
44
+
45
+ function SwitchField({
46
+ id,
47
+ label,
48
+ description,
49
+ checked,
50
+ onCheckedChange,
51
+ }: {
52
+ id: keyof AccessFormData;
53
+ label: string;
54
+ description: string;
55
+ checked: boolean;
56
+ onCheckedChange: (checked: boolean) => void;
57
+ }) {
58
+ return (
59
+ <div className="flex items-center justify-between rounded-lg border p-4">
60
+ <div className="space-y-0.5">
61
+ <Label htmlFor={id}>{label}</Label>
62
+ <p className="text-sm text-muted-foreground">{description}</p>
63
+ </div>
64
+ <Switch id={id} checked={checked} onCheckedChange={onCheckedChange} />
65
+ </div>
66
+ );
67
+ }
68
+
69
+ export function PortalAccessForm({ organizationId, initialSharing, initialSeo }: PortalAccessFormProps) {
70
+ const [saving, setSaving] = useState(false);
71
+ const [message, setMessage] = useState<{ type: "success" | "error"; text: string } | null>(null);
72
+
73
+ const form = useForm<AccessFormData>({
74
+ defaultValues: {
75
+ enabled: initialSharing?.enabled ?? true,
76
+ allowPublicVoting: initialSharing?.allowPublicVoting ?? true,
77
+ allowPublicComments: initialSharing?.allowPublicComments ?? false,
78
+ showVoteCount: initialSharing?.showVoteCount ?? true,
79
+ showAuthor: initialSharing?.showAuthor ?? false,
80
+ noIndex: initialSeo?.noIndex ?? false,
81
+ },
82
+ });
83
+ const enabled = useWatch({ control: form.control, name: "enabled" }) ?? true;
84
+ const allowPublicVoting = useWatch({ control: form.control, name: "allowPublicVoting" }) ?? true;
85
+ const allowPublicComments = useWatch({ control: form.control, name: "allowPublicComments" }) ?? false;
86
+ const showVoteCount = useWatch({ control: form.control, name: "showVoteCount" }) ?? true;
87
+ const showAuthor = useWatch({ control: form.control, name: "showAuthor" }) ?? false;
88
+ const noIndex = useWatch({ control: form.control, name: "noIndex" }) ?? false;
89
+
90
+ const onSubmit = async (data: AccessFormData) => {
91
+ setSaving(true);
92
+ setMessage(null);
93
+
94
+ const sharingPayload: PortalSharingConfig = {
95
+ ...initialSharing,
96
+ enabled: data.enabled,
97
+ allowPublicVoting: data.allowPublicVoting,
98
+ allowPublicComments: data.allowPublicComments,
99
+ showVoteCount: data.showVoteCount,
100
+ showAuthor: data.showAuthor,
101
+ };
102
+
103
+ const seoPayload: PortalSeoConfig = {
104
+ ...initialSeo,
105
+ noIndex: data.noIndex,
106
+ };
107
+
108
+ const sharingResult = await updatePortalSettings(organizationId, "sharing", sharingPayload);
109
+
110
+ if (!sharingResult.success) {
111
+ setMessage({ type: "error", text: sharingResult.error || "保存失败" });
112
+ setSaving(false);
113
+ return;
114
+ }
115
+
116
+ const seoResult = await updatePortalSettings(organizationId, "seo", seoPayload);
117
+
118
+ if (seoResult.success) {
119
+ setMessage({ type: "success", text: "可见性设置已保存" });
120
+ } else {
121
+ setMessage({ type: "error", text: seoResult.error || "保存失败" });
122
+ }
123
+
124
+ setSaving(false);
125
+ };
126
+
127
+ return (
128
+ <form onSubmit={form.handleSubmit(onSubmit)} className="space-y-6">
129
+ <SwitchField
130
+ id="enabled"
131
+ label="公开 Portal"
132
+ description="关闭后访客无法访问公开门户"
133
+ checked={enabled}
134
+ onCheckedChange={(checked) => form.setValue("enabled", checked)}
135
+ />
136
+
137
+ <SwitchField
138
+ id="allowPublicVoting"
139
+ label="允许公开投票"
140
+ description="访客无需登录即可投票"
141
+ checked={allowPublicVoting}
142
+ onCheckedChange={(checked) => form.setValue("allowPublicVoting", checked)}
143
+ />
144
+
145
+ <SwitchField
146
+ id="allowPublicComments"
147
+ label="允许公开评论"
148
+ description="访客无需登录即可评论"
149
+ checked={allowPublicComments}
150
+ onCheckedChange={(checked) => form.setValue("allowPublicComments", checked)}
151
+ />
152
+
153
+ <SwitchField
154
+ id="showVoteCount"
155
+ label="显示投票数量"
156
+ description="在反馈列表中显示投票计数"
157
+ checked={showVoteCount}
158
+ onCheckedChange={(checked) => form.setValue("showVoteCount", checked)}
159
+ />
160
+
161
+ <SwitchField
162
+ id="showAuthor"
163
+ label="显示提交者"
164
+ description="显示反馈提交者名称"
165
+ checked={showAuthor}
166
+ onCheckedChange={(checked) => form.setValue("showAuthor", checked)}
167
+ />
168
+
169
+ <SwitchField
170
+ id="noIndex"
171
+ label="阻止搜索引擎索引"
172
+ description="启用后搜索引擎将不会收录此页面"
173
+ checked={noIndex}
174
+ onCheckedChange={(checked) => form.setValue("noIndex", checked)}
175
+ />
176
+
177
+ <div className="flex items-center gap-4">
178
+ <Button type="submit" disabled={saving}>
179
+ {saving && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
180
+ 保存更改
181
+ </Button>
182
+ {message && (
183
+ <p
184
+ className={`text-sm ${
185
+ message.type === "success" ? "text-green-600" : "text-destructive"
186
+ }`}
187
+ >
188
+ {message.text}
189
+ </p>
190
+ )}
191
+ </div>
192
+ </form>
193
+ );
194
+ }
@@ -0,0 +1,95 @@
1
+ "use client";
2
+
3
+
4
+ /*
5
+ * Copyright (c) 2026 Echo Team
6
+ *
7
+ * This program is free software: you can redistribute it and/or modify
8
+ * it under the terms of the GNU Affero General Public License as published by
9
+ * the Free Software Foundation, either version 3 of the License, or
10
+ * (at your option) any later version.
11
+ *
12
+ * This program is distributed in the hope that it will be useful,
13
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
14
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15
+ * GNU Affero General Public License for more details.
16
+ *
17
+ * You should have received a copy of the GNU Affero General Public License
18
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
19
+ */
20
+
21
+ import { useState } from "react";
22
+ import { useTranslations } from "next-intl";
23
+ import { useForm } from "react-hook-form";
24
+ import { Button } from "@/components/ui/button";
25
+ import { Input } from "@/components/ui/input";
26
+ import { Label } from "@/components/ui/label";
27
+ import { Loader2 } from "lucide-react";
28
+ import { updatePortalSettings } from "@/lib/services/portal-settings";
29
+ import type { PortalCopyConfig } from "@/lib/db/schema";
30
+
31
+ interface CopyFormProps {
32
+ organizationId: string;
33
+ initialData?: PortalCopyConfig;
34
+ }
35
+
36
+ type CopyFormData = Pick<PortalCopyConfig, "title">;
37
+
38
+ export function CopyForm({ organizationId, initialData }: CopyFormProps) {
39
+ const t = useTranslations("settings.portal.branding.copyForm");
40
+ const [saving, setSaving] = useState(false);
41
+ const [message, setMessage] = useState<{ type: "success" | "error"; text: string } | null>(null);
42
+
43
+ const form = useForm<CopyFormData>({
44
+ defaultValues: {
45
+ title: initialData?.title ?? t("defaults.title"),
46
+ },
47
+ });
48
+
49
+ const onSubmit = async (data: CopyFormData) => {
50
+ setSaving(true);
51
+ setMessage(null);
52
+
53
+ const result = await updatePortalSettings(organizationId, "copy", data);
54
+
55
+ if (result.success) {
56
+ setMessage({ type: "success", text: t("status.saved") });
57
+ } else {
58
+ setMessage({ type: "error", text: result.error || t("status.saveFailed") });
59
+ }
60
+
61
+ setSaving(false);
62
+ };
63
+
64
+ return (
65
+ <form onSubmit={form.handleSubmit(onSubmit)} className="space-y-6">
66
+ {/* Title */}
67
+ <div className="space-y-2">
68
+ <Label htmlFor="title">{t("labels.title")}</Label>
69
+ <Input
70
+ id="title"
71
+ placeholder={t("placeholders.title")}
72
+ {...form.register("title")}
73
+ />
74
+ <p className="text-sm text-muted-foreground">{t("help.title")}</p>
75
+ </div>
76
+
77
+ {/* Submit */}
78
+ <div className="flex items-center gap-4">
79
+ <Button type="submit" disabled={saving}>
80
+ {saving && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
81
+ {t("buttons.save")}
82
+ </Button>
83
+ {message && (
84
+ <p
85
+ className={`text-sm ${
86
+ message.type === "success" ? "text-green-600" : "text-destructive"
87
+ }`}
88
+ >
89
+ {message.text}
90
+ </p>
91
+ )}
92
+ </div>
93
+ </form>
94
+ );
95
+ }
@@ -0,0 +1,23 @@
1
+ /*
2
+ * Copyright (c) 2026 Echo Team
3
+ *
4
+ * This program is free software: you can redistribute it and/or modify
5
+ * it under the terms of the GNU Affero General Public License as published by
6
+ * the Free Software Foundation, either version 3 of the License, or
7
+ * (at your option) any later version.
8
+ *
9
+ * This program is distributed in the hope that it will be useful,
10
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
11
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12
+ * GNU Affero General Public License for more details.
13
+ *
14
+ * You should have received a copy of the GNU Affero General Public License
15
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
16
+ */
17
+
18
+ export { ThemeForm } from "./theme-form";
19
+ export { CopyForm } from "./copy-form";
20
+ export { SeoForm } from "./seo-form";
21
+ export { SharingForm } from "./sharing-form";
22
+ export { LanguagesForm } from "./languages-form";
23
+ export { PortalAccessForm } from "./access-form";
@@ -0,0 +1,223 @@
1
+ "use client";
2
+
3
+
4
+ /*
5
+ * Copyright (c) 2026 Echo Team
6
+ *
7
+ * This program is free software: you can redistribute it and/or modify
8
+ * it under the terms of the GNU Affero General Public License as published by
9
+ * the Free Software Foundation, either version 3 of the License, or
10
+ * (at your option) any later version.
11
+ *
12
+ * This program is distributed in the hope that it will be useful,
13
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
14
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15
+ * GNU Affero General Public License for more details.
16
+ *
17
+ * You should have received a copy of the GNU Affero General Public License
18
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
19
+ */
20
+
21
+ import { useState } from "react";
22
+ import { useForm, useFieldArray, useWatch } from "react-hook-form";
23
+ import { Button } from "@/components/ui/button";
24
+ import { Label } from "@/components/ui/label";
25
+ import {
26
+ Select,
27
+ SelectContent,
28
+ SelectItem,
29
+ SelectTrigger,
30
+ SelectValue,
31
+ } from "@/components/ui/select";
32
+ import { Loader2, Plus, X } from "lucide-react";
33
+ import { updatePortalSettings } from "@/lib/services/portal-settings";
34
+
35
+ interface LanguagesFormProps {
36
+ organizationId: string;
37
+ initialLanguages?: string[];
38
+ initialDefaultLanguage?: string;
39
+ }
40
+
41
+ const availableLanguages = [
42
+ { code: "zh-CN", label: "简体中文" },
43
+ { code: "zh-TW", label: "繁體中文" },
44
+ { code: "en", label: "English" },
45
+ { code: "ja", label: "日本語" },
46
+ { code: "ko", label: "한국어" },
47
+ { code: "de", label: "Deutsch" },
48
+ { code: "fr", label: "Français" },
49
+ { code: "es", label: "Español" },
50
+ { code: "pt", label: "Português" },
51
+ { code: "ru", label: "Русский" },
52
+ ] as const;
53
+
54
+ type FormData = {
55
+ languages: { code: string }[];
56
+ defaultLanguage: string;
57
+ };
58
+
59
+ export function LanguagesForm({
60
+ organizationId,
61
+ initialLanguages,
62
+ initialDefaultLanguage,
63
+ }: LanguagesFormProps) {
64
+ const [saving, setSaving] = useState(false);
65
+ const [message, setMessage] = useState<{ type: "success" | "error"; text: string } | null>(null);
66
+
67
+ const form = useForm<FormData>({
68
+ defaultValues: {
69
+ languages: (initialLanguages ?? ["zh-CN"]).map((code) => ({ code })),
70
+ defaultLanguage: initialDefaultLanguage ?? "zh-CN",
71
+ },
72
+ });
73
+
74
+ const { fields, append, remove } = useFieldArray({
75
+ control: form.control,
76
+ name: "languages",
77
+ });
78
+
79
+ const watchLanguages = useWatch({ control: form.control, name: "languages" }) ?? [];
80
+ const defaultLanguage = useWatch({ control: form.control, name: "defaultLanguage" }) ?? "zh-CN";
81
+ const selectedCodes = watchLanguages.map((l) => l.code);
82
+
83
+ const onSubmit = async (data: FormData) => {
84
+ setSaving(true);
85
+ setMessage(null);
86
+
87
+ const languageCodes = data.languages.map((l) => l.code);
88
+
89
+ // Update languages array
90
+ const result = await updatePortalSettings(organizationId, "languages", languageCodes);
91
+
92
+ if (result.success) {
93
+ // Also update defaultLanguage
94
+ await updatePortalSettings(organizationId, "defaultLanguage" as never, data.defaultLanguage as never);
95
+ setMessage({ type: "success", text: "语言设置已保存" });
96
+ } else {
97
+ setMessage({ type: "error", text: result.error || "保存失败" });
98
+ }
99
+
100
+ setSaving(false);
101
+ };
102
+
103
+ const getLanguageLabel = (code: string) => {
104
+ return availableLanguages.find((l) => l.code === code)?.label ?? code;
105
+ };
106
+
107
+ return (
108
+ <form onSubmit={form.handleSubmit(onSubmit)} className="space-y-6">
109
+ {/* Default Language */}
110
+ <div className="space-y-2">
111
+ <Label htmlFor="defaultLanguage">默认语言</Label>
112
+ <Select
113
+ value={defaultLanguage}
114
+ onValueChange={(value) => form.setValue("defaultLanguage", value)}
115
+ >
116
+ <SelectTrigger id="defaultLanguage">
117
+ <SelectValue placeholder="选择默认语言" />
118
+ </SelectTrigger>
119
+ <SelectContent>
120
+ {watchLanguages.map((lang) => (
121
+ <SelectItem key={lang.code} value={lang.code}>
122
+ {getLanguageLabel(lang.code)}
123
+ </SelectItem>
124
+ ))}
125
+ </SelectContent>
126
+ </Select>
127
+ <p className="text-sm text-muted-foreground">Portal 加载时使用的语言</p>
128
+ </div>
129
+
130
+ {/* Supported Languages */}
131
+ <div className="space-y-4">
132
+ <div className="flex items-center justify-between">
133
+ <Label>支持的语言</Label>
134
+ <Button
135
+ type="button"
136
+ variant="outline"
137
+ size="sm"
138
+ onClick={() => {
139
+ const available = availableLanguages.find(
140
+ (l) => !selectedCodes.includes(l.code)
141
+ );
142
+ if (available) {
143
+ append({ code: available.code });
144
+ }
145
+ }}
146
+ disabled={selectedCodes.length >= availableLanguages.length}
147
+ >
148
+ <Plus className="mr-1 h-4 w-4" />
149
+ 添加语言
150
+ </Button>
151
+ </div>
152
+
153
+ <div className="space-y-2">
154
+ {fields.map((field, index) => (
155
+ <div key={field.id} className="flex items-center gap-2">
156
+ <Select
157
+ value={field.code}
158
+ onValueChange={(value) => {
159
+ form.setValue(`languages.${index}.code`, value);
160
+ }}
161
+ >
162
+ <SelectTrigger className="flex-1">
163
+ <SelectValue />
164
+ </SelectTrigger>
165
+ <SelectContent>
166
+ {availableLanguages
167
+ .filter(
168
+ (l) => l.code === field.code || !selectedCodes.includes(l.code)
169
+ )
170
+ .map((lang) => (
171
+ <SelectItem key={lang.code} value={lang.code}>
172
+ {lang.label}
173
+ </SelectItem>
174
+ ))}
175
+ </SelectContent>
176
+ </Select>
177
+ {fields.length > 1 && (
178
+ <Button
179
+ type="button"
180
+ variant="ghost"
181
+ size="icon"
182
+ onClick={() => {
183
+ // If removing default language, update default
184
+ if (defaultLanguage === field.code) {
185
+ const remaining = watchLanguages.filter((_, i) => i !== index);
186
+ if (remaining.length > 0) {
187
+ form.setValue("defaultLanguage", remaining[0].code);
188
+ }
189
+ }
190
+ remove(index);
191
+ }}
192
+ >
193
+ <X className="h-4 w-4" />
194
+ </Button>
195
+ )}
196
+ </div>
197
+ ))}
198
+ </div>
199
+
200
+ <p className="text-sm text-muted-foreground">
201
+ 用户可以在 Portal 中切换的语言选项
202
+ </p>
203
+ </div>
204
+
205
+ {/* Submit */}
206
+ <div className="flex items-center gap-4">
207
+ <Button type="submit" disabled={saving}>
208
+ {saving && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
209
+ 保存更改
210
+ </Button>
211
+ {message && (
212
+ <p
213
+ className={`text-sm ${
214
+ message.type === "success" ? "text-green-600" : "text-destructive"
215
+ }`}
216
+ >
217
+ {message.text}
218
+ </p>
219
+ )}
220
+ </div>
221
+ </form>
222
+ );
223
+ }