@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,279 @@
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 Link from "next/link";
22
+ import { usePathname, useRouter, useSearchParams } from "next/navigation";
23
+ import { useTranslations } from "next-intl";
24
+ import { cn } from "@/lib/utils";
25
+ import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
26
+ import {
27
+ DropdownMenu,
28
+ DropdownMenuContent,
29
+ DropdownMenuItem,
30
+ DropdownMenuSeparator,
31
+ DropdownMenuTrigger,
32
+ DropdownMenuSub,
33
+ DropdownMenuSubTrigger,
34
+ DropdownMenuSubContent,
35
+ DropdownMenuGroup,
36
+ } from "@/components/ui/dropdown-menu";
37
+ import {
38
+ LayoutDashboard,
39
+ MessageSquare,
40
+ Settings,
41
+ LogOut,
42
+ ChevronUp,
43
+ Building2,
44
+ Plus,
45
+ Check,
46
+ Languages,
47
+ } from "lucide-react";
48
+ import type { UserRole } from "@/lib/auth/permissions";
49
+ import { authClient } from "@/lib/auth/client";
50
+ import { useLocale } from "next-intl";
51
+ import { useTransition, useEffect, useRef } from "react";
52
+ import { LOCALE_COOKIE_NAME, SUPPORTED_LOCALES, type AppLocale } from "@/i18n/config";
53
+ import {
54
+ DropdownMenuRadioGroup,
55
+ DropdownMenuRadioItem,
56
+ } from "@/components/ui/dropdown-menu";
57
+
58
+ interface SidebarProps {
59
+ user: {
60
+ name: string;
61
+ email: string;
62
+ image?: string | null;
63
+ role: UserRole;
64
+ };
65
+ organizations?: Array<{ id: string; name: string; slug: string; role: string }>;
66
+ currentOrgId?: string | null;
67
+ onClose?: () => void;
68
+ }
69
+
70
+ const COOKIE_MAX_AGE_SECONDS = 60 * 60 * 24 * 30;
71
+
72
+ export function Sidebar({ user, organizations = [], currentOrgId, onClose }: SidebarProps) {
73
+ const pathname = usePathname();
74
+ const router = useRouter();
75
+ const searchParams = useSearchParams();
76
+ const t = useTranslations("navigation");
77
+ const tLang = useTranslations("language");
78
+ const locale = useLocale() as AppLocale;
79
+ const [isPending, startTransition] = useTransition();
80
+ const pendingOrgIdRef = useRef<string | null>(null);
81
+ const pendingLocaleRef = useRef<AppLocale | null>(null);
82
+
83
+ const navItems = [
84
+ { href: "/dashboard", label: t("dashboard"), icon: LayoutDashboard },
85
+ { href: "/admin/feedback", label: t("feedback"), icon: MessageSquare },
86
+ ];
87
+
88
+ const handleLinkClick = () => {
89
+ onClose?.();
90
+ };
91
+
92
+ const handleLogout = async () => {
93
+ await authClient.signOut({
94
+ fetchOptions: {
95
+ onSuccess: () => {
96
+ router.push("/");
97
+ },
98
+ },
99
+ });
100
+ onClose?.();
101
+ };
102
+
103
+ // Set organization cookie when pendingOrgId changes
104
+ useEffect(() => {
105
+ const orgId = pendingOrgIdRef.current;
106
+ if (!orgId) return;
107
+ const params = new URLSearchParams(searchParams);
108
+ params.set("organizationId", orgId);
109
+ document.cookie = `orgId=${orgId};path=/;max-age=${COOKIE_MAX_AGE_SECONDS};samesite=lax`;
110
+ router.replace(`/dashboard?${params.toString()}`);
111
+ startTransition(() => {
112
+ pendingOrgIdRef.current = null;
113
+ });
114
+ onClose?.();
115
+ }, [searchParams, router, onClose]);
116
+
117
+ const handleOrgSwitch = (orgId: string) => {
118
+ pendingOrgIdRef.current = orgId;
119
+ };
120
+
121
+ // Set locale cookie when pendingLocale changes
122
+ useEffect(() => {
123
+ const nextLocale = pendingLocaleRef.current;
124
+ if (!nextLocale) return;
125
+ if (nextLocale === locale) {
126
+ startTransition(() => {
127
+ pendingLocaleRef.current = null;
128
+ });
129
+ return;
130
+ }
131
+ const secure = window.location.protocol === "https:" ? ";secure" : "";
132
+ document.cookie = `${LOCALE_COOKIE_NAME}=${nextLocale};path=/;max-age=${60 * 60 * 24 * 365};samesite=lax${secure}`;
133
+ startTransition(() => {
134
+ router.refresh();
135
+ pendingLocaleRef.current = null;
136
+ });
137
+ }, [locale, router]);
138
+
139
+ const handleLocaleChange = (nextLocale: AppLocale) => {
140
+ pendingLocaleRef.current = nextLocale;
141
+ };
142
+
143
+ const currentOrg = organizations.find((org) => org.id === currentOrgId);
144
+
145
+ return (
146
+ <div className="flex h-full flex-col bg-background">
147
+ {/* Logo */}
148
+ <div className="flex h-14 items-center border-b px-4">
149
+ <Link href="/dashboard" className="flex items-center gap-2 font-semibold" onClick={handleLinkClick}>
150
+ <span className="text-xl">Echo</span>
151
+ </Link>
152
+ </div>
153
+
154
+ <div className="flex-1 overflow-auto py-4">
155
+ {/* Navigation */}
156
+ <div className="px-4 mb-4">
157
+ <span className="text-xs font-medium text-muted-foreground uppercase tracking-wider mb-2 block">
158
+ {t("sectionLabel")}
159
+ </span>
160
+ <nav className="space-y-1">
161
+ {navItems.map((item) => (
162
+ <Link
163
+ key={item.href}
164
+ href={item.href}
165
+ onClick={handleLinkClick}
166
+ className={cn(
167
+ "flex items-center gap-2 rounded-md px-2 py-1.5 text-sm transition-colors hover:bg-muted",
168
+ pathname === item.href && "bg-muted font-medium"
169
+ )}
170
+ >
171
+ <item.icon className="h-4 w-4 text-muted-foreground" />
172
+ {item.label}
173
+ </Link>
174
+ ))}
175
+ </nav>
176
+ </div>
177
+
178
+ </div>
179
+
180
+ {/* User Profile Dropdown */}
181
+ <div className="border-t p-4">
182
+ <DropdownMenu>
183
+ <DropdownMenuTrigger asChild>
184
+ <button className="flex w-full items-center gap-3 rounded-lg px-2 py-2 hover:bg-muted transition-colors">
185
+ <Avatar className="h-9 w-9">
186
+ <AvatarImage src={user.image || undefined} alt={user.name} />
187
+ <AvatarFallback>{user.name.slice(0, 2).toUpperCase()}</AvatarFallback>
188
+ </Avatar>
189
+ <div className="flex-1 min-w-0 text-left">
190
+ <p className="text-sm font-medium truncate">{user.name}</p>
191
+ <p className="text-xs text-muted-foreground truncate">
192
+ {currentOrg?.name || user.email}
193
+ </p>
194
+ </div>
195
+ <ChevronUp className="h-4 w-4 text-muted-foreground" />
196
+ </button>
197
+ </DropdownMenuTrigger>
198
+ <DropdownMenuContent align="end" className="w-56">
199
+ {/* Settings - Most commonly used */}
200
+ <DropdownMenuGroup>
201
+ <DropdownMenuItem asChild>
202
+ <Link href="/settings" onClick={handleLinkClick}>
203
+ <Settings className="mr-2 h-4 w-4" />
204
+ {t("settings")}
205
+ </Link>
206
+ </DropdownMenuItem>
207
+ </DropdownMenuGroup>
208
+
209
+ <DropdownMenuSeparator />
210
+
211
+ {/* Organization Switcher */}
212
+ {organizations.length > 0 && (
213
+ <DropdownMenuGroup>
214
+ <DropdownMenuSub>
215
+ <DropdownMenuSubTrigger>
216
+ <Building2 className="mr-2 h-4 w-4" />
217
+ <span className="flex-1 truncate">{currentOrg?.name || t("selectOrganization")}</span>
218
+ </DropdownMenuSubTrigger>
219
+ <DropdownMenuSubContent className="w-48">
220
+ {organizations.map((org) => (
221
+ <DropdownMenuItem
222
+ key={org.id}
223
+ onClick={() => handleOrgSwitch(org.id)}
224
+ className="flex items-center justify-between"
225
+ >
226
+ <span className="truncate">{org.name}</span>
227
+ {org.id === currentOrgId && (
228
+ <Check className="ml-2 h-4 w-4 text-primary flex-shrink-0" />
229
+ )}
230
+ </DropdownMenuItem>
231
+ ))}
232
+ <DropdownMenuSeparator />
233
+ <DropdownMenuItem asChild>
234
+ <Link href="/settings/organizations/new" onClick={handleLinkClick}>
235
+ <Plus className="mr-2 h-4 w-4" />
236
+ {t("newOrganization")}
237
+ </Link>
238
+ </DropdownMenuItem>
239
+ </DropdownMenuSubContent>
240
+ </DropdownMenuSub>
241
+ </DropdownMenuGroup>
242
+ )}
243
+
244
+ {/* Language Switcher */}
245
+ <DropdownMenuGroup>
246
+ <DropdownMenuSub>
247
+ <DropdownMenuSubTrigger disabled={isPending}>
248
+ <Languages className="mr-2 h-4 w-4" />
249
+ {tLang("label")}
250
+ </DropdownMenuSubTrigger>
251
+ <DropdownMenuSubContent>
252
+ <DropdownMenuRadioGroup value={locale}>
253
+ {SUPPORTED_LOCALES.map((supportedLocale) => (
254
+ <DropdownMenuRadioItem
255
+ key={supportedLocale}
256
+ value={supportedLocale}
257
+ onClick={() => handleLocaleChange(supportedLocale)}
258
+ >
259
+ {tLang(supportedLocale)}
260
+ </DropdownMenuRadioItem>
261
+ ))}
262
+ </DropdownMenuRadioGroup>
263
+ </DropdownMenuSubContent>
264
+ </DropdownMenuSub>
265
+ </DropdownMenuGroup>
266
+
267
+ <DropdownMenuSeparator />
268
+
269
+ {/* Logout - Destructive action at the bottom */}
270
+ <DropdownMenuItem onClick={handleLogout} className="text-destructive focus:text-destructive">
271
+ <LogOut className="mr-2 h-4 w-4" />
272
+ {t("logout")}
273
+ </DropdownMenuItem>
274
+ </DropdownMenuContent>
275
+ </DropdownMenu>
276
+ </div>
277
+ </div>
278
+ );
279
+ }
@@ -0,0 +1,132 @@
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 Image from "next/image";
22
+ import { Badge } from "@/components/ui/badge";
23
+ import { Card, CardContent, CardHeader } from "@/components/ui/card";
24
+ import { cn } from "@/lib/utils";
25
+
26
+ export interface ChangelogItem {
27
+ id: string;
28
+ title: string;
29
+ content: string;
30
+ type: "feature" | "improvement" | "fix" | "announcement";
31
+ publishedAt: string;
32
+ image?: string | null;
33
+ author?: {
34
+ name: string;
35
+ image?: string | null;
36
+ } | null;
37
+ }
38
+
39
+ interface ChangelogEntryProps {
40
+ entry: ChangelogItem;
41
+ className?: string;
42
+ }
43
+
44
+ const typeConfig: Record<
45
+ ChangelogItem["type"],
46
+ { label: string; className: string }
47
+ > = {
48
+ feature: {
49
+ label: "New Feature",
50
+ className: "bg-green-100 text-green-800 dark:bg-green-900/30 dark:text-green-300",
51
+ },
52
+ improvement: {
53
+ label: "Improvement",
54
+ className: "bg-blue-100 text-blue-800 dark:bg-blue-900/30 dark:text-blue-300",
55
+ },
56
+ fix: {
57
+ label: "Bug Fix",
58
+ className: "bg-orange-100 text-orange-800 dark:bg-orange-900/30 dark:text-orange-300",
59
+ },
60
+ announcement: {
61
+ label: "Announcement",
62
+ className: "bg-purple-100 text-purple-800 dark:bg-purple-900/30 dark:text-purple-300",
63
+ },
64
+ };
65
+
66
+ export function ChangelogEntry({ entry, className }: ChangelogEntryProps) {
67
+ const typeInfo = typeConfig[entry.type];
68
+
69
+ return (
70
+ <Card className={cn("overflow-hidden", className)}>
71
+ {/* Image */}
72
+ {entry.image && (
73
+ <div className="relative aspect-video bg-muted">
74
+ <Image
75
+ src={entry.image}
76
+ alt={entry.title}
77
+ fill
78
+ className="object-cover"
79
+ />
80
+ </div>
81
+ )}
82
+
83
+ <CardHeader className="space-y-2">
84
+ {/* Date and Type */}
85
+ <div className="flex flex-wrap items-center gap-2 text-sm">
86
+ <time
87
+ dateTime={entry.publishedAt}
88
+ className="text-muted-foreground"
89
+ >
90
+ {new Date(entry.publishedAt).toLocaleDateString("en-US", {
91
+ month: "long",
92
+ day: "numeric",
93
+ year: "numeric",
94
+ })}
95
+ </time>
96
+ <Badge variant="secondary" className={cn("text-xs", typeInfo.className)}>
97
+ {typeInfo.label}
98
+ </Badge>
99
+ </div>
100
+
101
+ {/* Title */}
102
+ <h3 className="text-xl font-semibold tracking-tight">{entry.title}</h3>
103
+ </CardHeader>
104
+
105
+ <CardContent>
106
+ {/* Content */}
107
+ <div
108
+ className="prose prose-sm dark:prose-invert max-w-none"
109
+ dangerouslySetInnerHTML={{ __html: entry.content }}
110
+ />
111
+
112
+ {/* Author */}
113
+ {entry.author && (
114
+ <div className="flex items-center gap-2 mt-4 pt-4 border-t">
115
+ {entry.author.image && (
116
+ <Image
117
+ src={entry.author.image}
118
+ alt={entry.author.name}
119
+ width={24}
120
+ height={24}
121
+ className="rounded-full"
122
+ />
123
+ )}
124
+ <span className="text-sm text-muted-foreground">
125
+ Posted by {entry.author.name}
126
+ </span>
127
+ </div>
128
+ )}
129
+ </CardContent>
130
+ </Card>
131
+ );
132
+ }
@@ -0,0 +1,85 @@
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 { ChangelogEntry, type ChangelogItem } from "./changelog-entry";
22
+ import { cn } from "@/lib/utils";
23
+
24
+ interface ChangelogListProps {
25
+ entries: ChangelogItem[];
26
+ className?: string;
27
+ }
28
+
29
+ export function ChangelogList({ entries, className }: ChangelogListProps) {
30
+ if (entries.length === 0) {
31
+ return (
32
+ <div className="text-center py-16">
33
+ <h3 className="text-lg font-semibold text-muted-foreground">
34
+ No updates yet
35
+ </h3>
36
+ <p className="text-sm text-muted-foreground mt-1">
37
+ Check back later for product updates and announcements.
38
+ </p>
39
+ </div>
40
+ );
41
+ }
42
+
43
+ // Group entries by month/year
44
+ const groupedEntries = entries.reduce(
45
+ (acc, entry) => {
46
+ const date = new Date(entry.publishedAt);
47
+ const key = date.toLocaleDateString("en-US", {
48
+ year: "numeric",
49
+ month: "long",
50
+ });
51
+ if (!acc[key]) {
52
+ acc[key] = [];
53
+ }
54
+ acc[key].push(entry);
55
+ return acc;
56
+ },
57
+ {} as Record<string, ChangelogItem[]>
58
+ );
59
+
60
+ return (
61
+ <div className={cn("max-w-3xl mx-auto space-y-12", className)}>
62
+ {Object.entries(groupedEntries).map(([month, monthEntries], index) => (
63
+ <div
64
+ key={month}
65
+ className={cn(
66
+ "pt-8 border-t border-border",
67
+ index === 0 && "pt-0 border-0"
68
+ )}
69
+ >
70
+ {/* Month Header */}
71
+ <h2 className="text-lg font-semibold text-muted-foreground mb-6 sticky top-20 bg-background/95 py-2 backdrop-blur">
72
+ {month}
73
+ </h2>
74
+
75
+ {/* Entries */}
76
+ <div className="space-y-8">
77
+ {monthEntries.map((entry) => (
78
+ <ChangelogEntry key={entry.id} entry={entry} />
79
+ ))}
80
+ </div>
81
+ </div>
82
+ ))}
83
+ </div>
84
+ );
85
+ }
@@ -0,0 +1,29 @@
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
+ import { getContributorLevel } from "@/lib/portal/contributors";
19
+
20
+ interface ContributorBadgeProps {
21
+ votes: number;
22
+ comments: number;
23
+ accepted: number;
24
+ }
25
+
26
+ export function ContributorBadge(props: ContributorBadgeProps) {
27
+ const level = getContributorLevel(props);
28
+ return <span className="contributor-badge">Lv.{level}</span>;
29
+ }
@@ -0,0 +1,98 @@
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 { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
22
+ import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
23
+ import { cn } from "@/lib/utils";
24
+ import { useTranslations } from "next-intl";
25
+
26
+ export interface Contributor {
27
+ id: string;
28
+ name: string;
29
+ image?: string | null;
30
+ points: number;
31
+ }
32
+
33
+ interface ContributorsSidebarProps {
34
+ contributors: Contributor[];
35
+ className?: string;
36
+ }
37
+
38
+ export function ContributorsSidebar({
39
+ contributors,
40
+ className,
41
+ }: ContributorsSidebarProps) {
42
+ const t = useTranslations("portal.contributors");
43
+ return (
44
+ <Card className={cn("sticky top-24", className)}>
45
+ <CardHeader className="pb-3">
46
+ <CardTitle className="text-base font-semibold">{t("mostHelpful")}</CardTitle>
47
+ </CardHeader>
48
+ <CardContent className="space-y-3">
49
+ {contributors.slice(0, 10).map((contributor, index) => (
50
+ <div
51
+ key={contributor.id}
52
+ className="flex items-center gap-3"
53
+ >
54
+ <span className="w-5 text-center text-sm font-medium text-muted-foreground">
55
+ {index + 1}
56
+ </span>
57
+ <Avatar className="h-8 w-8">
58
+ <AvatarImage
59
+ src={contributor.image ?? undefined}
60
+ alt={contributor.name}
61
+ />
62
+ <AvatarFallback className="text-xs">
63
+ {contributor.name
64
+ .split(" ")
65
+ .map((n) => n[0])
66
+ .join("")
67
+ .toUpperCase()
68
+ .slice(0, 2)}
69
+ </AvatarFallback>
70
+ </Avatar>
71
+ <div className="flex-1 min-w-0">
72
+ <p className="text-sm font-medium truncate">{contributor.name}</p>
73
+ </div>
74
+ <span className="text-sm font-semibold text-primary">
75
+ {contributor.points}
76
+ </span>
77
+ </div>
78
+ ))}
79
+
80
+ {contributors.length === 0 && (
81
+ <p className="text-sm text-muted-foreground text-center py-4">
82
+ No contributors yet
83
+ </p>
84
+ )}
85
+ </CardContent>
86
+
87
+ {/* Powered by badge */}
88
+ <div className="px-6 pb-4">
89
+ <div className="pt-4 border-t">
90
+ <p className="text-xs text-muted-foreground text-center">
91
+ Powered by{" "}
92
+ <span className="font-semibold text-foreground">Echo</span>
93
+ </p>
94
+ </div>
95
+ </div>
96
+ </Card>
97
+ );
98
+ }