@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.
- package/.changeset/README.md +21 -0
- package/.changeset/config.json +11 -0
- package/.changeset/cozy-ghosts-care.md +5 -0
- package/.changeset/sharp-lines-stand.md +5 -0
- package/.changeset/sour-doodles-eat.md +5 -0
- package/.changeset/tender-moose-shop.md +5 -0
- package/.github/pull_request_template.md +13 -0
- package/.github/workflows/ci.yml +41 -0
- package/.github/workflows/publish.yml +44 -0
- package/.github/workflows/release.yml +73 -0
- package/AGENTS.md +92 -0
- package/CHANGELOG.md +13 -0
- package/Dockerfile +57 -0
- package/LICENSE +661 -0
- package/Makefile +77 -0
- package/README.md +198 -0
- package/app/(auth)/login/page.tsx +53 -0
- package/app/(auth)/register/page.tsx +48 -0
- package/app/(auth)/sign-in/page.tsx +22 -0
- package/app/(dashboard)/admin/feedback/[id]/edit/page.tsx +103 -0
- package/app/(dashboard)/admin/feedback/[id]/page.tsx +154 -0
- package/app/(dashboard)/admin/feedback/new/page.tsx +91 -0
- package/app/(dashboard)/admin/feedback/page.tsx +81 -0
- package/app/(dashboard)/admin/layout.tsx +48 -0
- package/app/(dashboard)/analytics/portal/page.tsx +30 -0
- package/app/(dashboard)/dashboard/page.tsx +133 -0
- package/app/(dashboard)/layout.tsx +69 -0
- package/app/(dashboard)/no-access/page.tsx +45 -0
- package/app/(dashboard)/settings/access/page.tsx +56 -0
- package/app/(dashboard)/settings/api-keys/page.tsx +55 -0
- package/app/(dashboard)/settings/appearance/page.tsx +40 -0
- package/app/(dashboard)/settings/branding/page.tsx +62 -0
- package/app/(dashboard)/settings/changelog/page.tsx +51 -0
- package/app/(dashboard)/settings/danger-zone/page.tsx +92 -0
- package/app/(dashboard)/settings/feedback/page.tsx +63 -0
- package/app/(dashboard)/settings/integrations/page.tsx +94 -0
- package/app/(dashboard)/settings/layout.tsx +43 -0
- package/app/(dashboard)/settings/modules/page.tsx +54 -0
- package/app/(dashboard)/settings/notifications/page.tsx +48 -0
- package/app/(dashboard)/settings/organization/page.tsx +104 -0
- package/app/(dashboard)/settings/organization/portal/access/page.tsx +22 -0
- package/app/(dashboard)/settings/organization/portal/experience/page.tsx +22 -0
- package/app/(dashboard)/settings/organization/portal/growth/page.tsx +22 -0
- package/app/(dashboard)/settings/organization/portal/layout.tsx +24 -0
- package/app/(dashboard)/settings/organization/portal/page.tsx +22 -0
- package/app/(dashboard)/settings/organizations/[orgId]/members/page.tsx +69 -0
- package/app/(dashboard)/settings/organizations/new/page.tsx +36 -0
- package/app/(dashboard)/settings/page.tsx +22 -0
- package/app/(dashboard)/settings/portal-access/page.tsx +53 -0
- package/app/(dashboard)/settings/portal-branding/page.tsx +59 -0
- package/app/(dashboard)/settings/portal-growth/page.tsx +57 -0
- package/app/(dashboard)/settings/portal-modules/page.tsx +49 -0
- package/app/(dashboard)/settings/portal-resources/page.tsx +66 -0
- package/app/(dashboard)/settings/profile/page.tsx +48 -0
- package/app/(dashboard)/settings/widgets/page.tsx +63 -0
- package/app/(public)/[organizationSlug]/changelog/page.tsx +109 -0
- package/app/(public)/[organizationSlug]/feedback/[id]/page.tsx +146 -0
- package/app/(public)/[organizationSlug]/page.tsx +160 -0
- package/app/(public)/[organizationSlug]/roadmap/page.tsx +142 -0
- package/app/(public)/docs/page.tsx +48 -0
- package/app/(public)/feedback/[id]/not-found.tsx +33 -0
- package/app/(public)/feedback/[id]/page.tsx +102 -0
- package/app/(public)/invite/[token]/page.tsx +121 -0
- package/app/(public)/page.tsx +22 -0
- package/app/(public)/widget/[organizationId]/page.tsx +122 -0
- package/app/api/_utils.ts +29 -0
- package/app/api/admin/backup/route.ts +72 -0
- package/app/api/api-keys/[keyId]/route.ts +92 -0
- package/app/api/api-keys/route.ts +116 -0
- package/app/api/auth/[...all]/route.ts +21 -0
- package/app/api/auth/clear-session/route.ts +43 -0
- package/app/api/auth/register/handler.ts +176 -0
- package/app/api/auth/register/route.ts +26 -0
- package/app/api/docs/route.ts +28 -0
- package/app/api/feedback/[id]/comments/[commentId]/route.ts +105 -0
- package/app/api/feedback/[id]/comments/route.ts +421 -0
- package/app/api/feedback/[id]/duplicates/route.ts +285 -0
- package/app/api/feedback/[id]/handler.ts +91 -0
- package/app/api/feedback/[id]/processing-status/route.ts +199 -0
- package/app/api/feedback/[id]/reclassify/route.ts +145 -0
- package/app/api/feedback/[id]/route.ts +511 -0
- package/app/api/feedback/[id]/suggest-tags/route.ts +227 -0
- package/app/api/feedback/[id]/sync-github/route.ts +52 -0
- package/app/api/feedback/[id]/vote/route.ts +431 -0
- package/app/api/feedback/bulk/route.ts +212 -0
- package/app/api/feedback/handler.ts +138 -0
- package/app/api/feedback/route.ts +298 -0
- package/app/api/feedback/similar/route.ts +100 -0
- package/app/api/health/route.test.ts +64 -0
- package/app/api/health/route.ts +92 -0
- package/app/api/identify/jwt/route.ts +29 -0
- package/app/api/integrations/github/route.ts +196 -0
- package/app/api/internal/domain-lookup/route.ts +67 -0
- package/app/api/invitations/accept/handler.ts +101 -0
- package/app/api/invitations/accept/route.ts +29 -0
- package/app/api/notifications/preferences/route.ts +109 -0
- package/app/api/organizations/[orgId]/handler.ts +123 -0
- package/app/api/organizations/[orgId]/invitations/handler.ts +121 -0
- package/app/api/organizations/[orgId]/invitations/route.ts +29 -0
- package/app/api/organizations/[orgId]/members/[memberId]/handler.ts +208 -0
- package/app/api/organizations/[orgId]/members/[memberId]/route.ts +30 -0
- package/app/api/organizations/[orgId]/members/handler.ts +77 -0
- package/app/api/organizations/[orgId]/members/route.ts +29 -0
- package/app/api/organizations/[orgId]/route.ts +30 -0
- package/app/api/organizations/handler.ts +97 -0
- package/app/api/organizations/route.ts +29 -0
- package/app/api/tags/sync/route.ts +88 -0
- package/app/api/upload/handler.ts +79 -0
- package/app/api/upload/route.ts +37 -0
- package/app/api/v1/feedback/[id]/route.ts +276 -0
- package/app/api/v1/feedback/route.ts +250 -0
- package/app/api/v1/spec/route.ts +356 -0
- package/app/api/webhooks/[webhookId]/route.ts +213 -0
- package/app/api/webhooks/github/route.ts +158 -0
- package/app/api/webhooks/route.ts +143 -0
- package/app/favicon.ico +0 -0
- package/app/globals.css +139 -0
- package/app/health/route.ts +108 -0
- package/app/layout.tsx +60 -0
- package/bun.lock +2503 -0
- package/components/api/rate-limit-info.tsx +86 -0
- package/components/api-keys/api-key-manager.tsx +262 -0
- package/components/auth/login-form.tsx +207 -0
- package/components/auth/register-form.tsx +230 -0
- package/components/comment/comment-form.tsx +111 -0
- package/components/comment/internal-notes.tsx +219 -0
- package/components/comment/public-comments.tsx +387 -0
- package/components/component-example-client-only.tsx +29 -0
- package/components/component-example.tsx +519 -0
- package/components/dashboard/index.ts +22 -0
- package/components/dashboard/organization-switcher.tsx +96 -0
- package/components/dashboard/quick-actions.tsx +57 -0
- package/components/dashboard/recent-feedback-list.tsx +152 -0
- package/components/dashboard/stats-cards.tsx +88 -0
- package/components/dashboard/status-chart.tsx +106 -0
- package/components/example.tsx +70 -0
- package/components/feedback/attachment-list.tsx +103 -0
- package/components/feedback/auto-classification-badge.tsx +92 -0
- package/components/feedback/classification-override.tsx +64 -0
- package/components/feedback/duplicate-suggestions-inline.tsx +158 -0
- package/components/feedback/duplicate-suggestions.tsx +188 -0
- package/components/feedback/embedded-feedback-form.tsx +439 -0
- package/components/feedback/feedback-actions.tsx +160 -0
- package/components/feedback/feedback-bulk-actions.tsx +184 -0
- package/components/feedback/feedback-detail-view.tsx +321 -0
- package/components/feedback/feedback-detail.tsx +305 -0
- package/components/feedback/feedback-edit-form.tsx +131 -0
- package/components/feedback/feedback-filters.tsx +222 -0
- package/components/feedback/feedback-list-controls.tsx +433 -0
- package/components/feedback/feedback-list-item.tsx +298 -0
- package/components/feedback/feedback-list-skeleton.tsx +49 -0
- package/components/feedback/feedback-list.tsx +523 -0
- package/components/feedback/feedback-sorter.tsx +117 -0
- package/components/feedback/feedback-stats.tsx +124 -0
- package/components/feedback/file-upload.tsx +289 -0
- package/components/feedback/processing-status.tsx +161 -0
- package/components/feedback/status-history.tsx +134 -0
- package/components/feedback/status-selector.tsx +153 -0
- package/components/feedback/submit-on-behalf-form.tsx +403 -0
- package/components/feedback/tag-suggestions.tsx +212 -0
- package/components/feedback/vote-button.tsx +113 -0
- package/components/feedback/vote-list.tsx +108 -0
- package/components/integrations/github-config.tsx +200 -0
- package/components/landing/hero.tsx +150 -0
- package/components/layout/dashboard-layout.tsx +59 -0
- package/components/layout/index.ts +20 -0
- package/components/layout/language-switcher.tsx +129 -0
- package/components/layout/mobile-sidebar.tsx +66 -0
- package/components/layout/sidebar.tsx +279 -0
- package/components/portal/changelog-entry.tsx +132 -0
- package/components/portal/changelog-list.tsx +85 -0
- package/components/portal/contributor-badge.tsx +29 -0
- package/components/portal/contributors-sidebar.tsx +98 -0
- package/components/portal/create-post-dialog.tsx +247 -0
- package/components/portal/feedback-board.tsx +205 -0
- package/components/portal/feedback-post-card.tsx +198 -0
- package/components/portal/help-center.tsx +169 -0
- package/components/portal/leaderboard.tsx +29 -0
- package/components/portal/portal-header.tsx +153 -0
- package/components/portal/portal-layout.tsx +62 -0
- package/components/portal/portal-modules-panel.tsx +118 -0
- package/components/portal/portal-nav.tsx +59 -0
- package/components/portal/portal-overview.tsx +174 -0
- package/components/portal/portal-settings-nav.tsx +62 -0
- package/components/portal/portal-settings-shell.tsx +71 -0
- package/components/portal/portal-shell.tsx +62 -0
- package/components/portal/portal-tab-nav.tsx +77 -0
- package/components/portal/project-switcher.tsx +20 -0
- package/components/portal/roadmap-board.tsx +82 -0
- package/components/portal/roadmap-card.tsx +76 -0
- package/components/portal/roadmap-column.tsx +78 -0
- package/components/portal/settings-forms/access-form.tsx +194 -0
- package/components/portal/settings-forms/copy-form.tsx +95 -0
- package/components/portal/settings-forms/index.ts +23 -0
- package/components/portal/settings-forms/languages-form.tsx +223 -0
- package/components/portal/settings-forms/seo-form.tsx +156 -0
- package/components/portal/settings-forms/sharing-form.tsx +155 -0
- package/components/portal/settings-forms/theme-form.tsx +104 -0
- package/components/settings/api-keys-list.tsx +167 -0
- package/components/settings/appearance-form.tsx +71 -0
- package/components/settings/index.ts +25 -0
- package/components/settings/invite-member-form.tsx +119 -0
- package/components/settings/notification-preferences.tsx +174 -0
- package/components/settings/organization-form.tsx +165 -0
- package/components/settings/organization-members-list.tsx +197 -0
- package/components/settings/profile-form.tsx +124 -0
- package/components/settings/role-selector.tsx +57 -0
- package/components/settings/settings-sidebar.tsx +115 -0
- package/components/shared/pagination.tsx +215 -0
- package/components/ui/alert-dialog.tsx +201 -0
- package/components/ui/alert.tsx +75 -0
- package/components/ui/avatar.tsx +126 -0
- package/components/ui/badge.tsx +62 -0
- package/components/ui/button.tsx +77 -0
- package/components/ui/card.tsx +111 -0
- package/components/ui/combobox.tsx +311 -0
- package/components/ui/dialog.tsx +158 -0
- package/components/ui/dropdown-menu.tsx +272 -0
- package/components/ui/field.tsx +256 -0
- package/components/ui/input-group.tsx +164 -0
- package/components/ui/input.tsx +36 -0
- package/components/ui/label.tsx +41 -0
- package/components/ui/pagination.tsx +142 -0
- package/components/ui/select.tsx +202 -0
- package/components/ui/separator.tsx +45 -0
- package/components/ui/sheet.tsx +151 -0
- package/components/ui/skeleton.tsx +32 -0
- package/components/ui/switch.tsx +49 -0
- package/components/ui/table.tsx +118 -0
- package/components/ui/tabs.tsx +107 -0
- package/components/ui/textarea.tsx +35 -0
- package/components/ui/tooltip.tsx +78 -0
- package/components/widget/widget-form.tsx +439 -0
- package/components.json +24 -0
- package/db/init/01-init.sql +13 -0
- package/docker-compose.dev.yml +26 -0
- package/docker-compose.yml +98 -0
- package/docs/architecture.md +259 -0
- package/docs/component-inventory.md +261 -0
- package/docs/database-migrations.md +76 -0
- package/docs/development-guide.md +209 -0
- package/docs/e2e-user-flows.csv +31 -0
- package/docs/er-diagram-feedback.mmd +138 -0
- package/docs/er-diagram.mmd +281 -0
- package/docs/i18n-check-report.md +296 -0
- package/docs/index.md +214 -0
- package/docs/logic-chain.md +94 -0
- package/docs/plans/2026-01-02-database-migration-scripts.md +496 -0
- package/docs/plans/2026-01-02-user-login-design.md +37 -0
- package/docs/plans/2026-01-02-user-login.md +437 -0
- package/docs/plans/2026-01-02-user-registration-design.md +47 -0
- package/docs/plans/2026-01-02-user-registration.md +628 -0
- package/docs/plans/2026-01-03-roles-permissions-design.md +20 -0
- package/docs/plans/2026-01-03-roles-permissions.md +266 -0
- package/docs/plans/2026-01-05-authentication-middleware.md +207 -0
- package/docs/plans/2026-01-05-member-removal.md +186 -0
- package/docs/plans/2026-01-05-organization-creation.md +374 -0
- package/docs/plans/2026-01-05-rbac-middleware.md +112 -0
- package/docs/plans/2026-01-05-role-configuration.md +441 -0
- package/docs/plans/2026-01-06-file-upload-support.md +804 -0
- package/docs/plans/2026-01-06-permission-check-hook.md +155 -0
- package/docs/plans/2026-01-06-resource-ownership-check.md +231 -0
- package/docs/plans/2026-01-07-feedback-tracking-link.md +459 -0
- package/docs/plans/2026-01-09-logout-redirect-design.md +52 -0
- package/docs/plans/2026-01-09-phase2-3-plan.md +654 -0
- package/docs/plans/2026-01-09-portal-execution-plan.md +408 -0
- package/docs/plans/2026-01-09-project-delete-feature-design.md +163 -0
- package/docs/plans/2026-01-09-project-delete-implementation.md +451 -0
- package/docs/plans/2026-01-09-project-edit-delete-design.md +52 -0
- package/docs/plans/2026-01-09-settings-center-design.md +114 -0
- package/docs/plans/2026-01-09-settings-center.md +948 -0
- package/docs/plans/2026-01-10-organization-only-design.md +66 -0
- package/docs/plans/2026-01-10-organization-only-implementation.md +433 -0
- package/docs/plans/2026-01-10-portal-settings-restructure-plan.md +18 -0
- package/docs/plans/2026-01-10-project-settings-tabs-design-implementation.md +296 -0
- package/docs/plans/2026-01-14-e2e-playwright-feedback.md +173 -0
- package/docs/plans/2026-01-15-feedback-management-org-context-design.md +82 -0
- package/docs/plans/2026-01-15-feedback-management-org-context-implementation-plan.md +521 -0
- package/docs/plans/2026-01-16-admin-feedback-filters-design.md +75 -0
- package/docs/plans/2026-01-16-admin-feedback-filters-implementation.md +293 -0
- package/docs/plans/2026-01-16-admin-feedback-route-consolidation.md +180 -0
- package/docs/plans/2026-01-16-e2e-test-fixes.md +158 -0
- package/docs/plans/2026-01-17-admin-feedback-filters.md +214 -0
- package/docs/plans/2026-01-17-admin-feedback-improvements.md +453 -0
- package/docs/plans/2026-01-18-changesets-design.md +40 -0
- package/docs/product_changes.md +37 -0
- package/docs/project-overview.md +159 -0
- package/docs/project-scan-report.json +104 -0
- package/docs/route-role-visibility.md +51 -0
- package/docs/source-tree-analysis.md +150 -0
- package/docs/testing/delete-project-manual-tests.md +18 -0
- package/docs/user-story-tracking.md +191 -0
- package/drizzle.config.ts +32 -0
- package/eslint.config.mjs +19 -0
- package/hooks/use-permissions.ts +56 -0
- package/i18n/config.ts +45 -0
- package/i18n/request.ts +28 -0
- package/i18n/resolve-locale.ts +38 -0
- package/lib/api/errors.ts +62 -0
- package/lib/auth/cli-config.ts +35 -0
- package/lib/auth/client.ts +20 -0
- package/lib/auth/config.ts +55 -0
- package/lib/auth/jwt-identity.ts +21 -0
- package/lib/auth/org-context.ts +71 -0
- package/lib/auth/organization.ts +107 -0
- package/lib/auth/permissions.ts +87 -0
- package/lib/auth/session.ts +23 -0
- package/lib/config/rate-limits.ts +64 -0
- package/lib/dashboard/get-dashboard-stats.ts +136 -0
- package/lib/db/index.ts +41 -0
- package/lib/db/migrate.test.ts +49 -0
- package/lib/db/migrate.ts +62 -0
- package/lib/db/migrations/.gitkeep +0 -0
- package/lib/db/migrations/0000_cynical_gladiator.sql +53 -0
- package/lib/db/migrations/0001_wandering_sunfire.sql +27 -0
- package/lib/db/migrations/0002_shallow_speedball.sql +1 -0
- package/lib/db/migrations/0003_add_org_description.sql +1 -0
- package/lib/db/migrations/0003_boring_wild_pack.sql +13 -0
- package/lib/db/migrations/0004_windy_tyrannus.sql +27 -0
- package/lib/db/migrations/0005_perpetual_doorman.sql +5 -0
- package/lib/db/migrations/0006_aberrant_captain_midlands.sql +13 -0
- package/lib/db/migrations/0007_clever_captain_cross.sql +14 -0
- package/lib/db/migrations/0008_sparkling_pandemic.sql +2 -0
- package/lib/db/migrations/0009_happy_black_tom.sql +29 -0
- package/lib/db/migrations/0010_kind_junta.sql +8 -0
- package/lib/db/migrations/0011_mute_squadron_supreme.sql +25 -0
- package/lib/db/migrations/0012_giant_power_man.sql +24 -0
- package/lib/db/migrations/0013_damp_titanium_man.sql +17 -0
- package/lib/db/migrations/0014_blue_alice.sql +18 -0
- package/lib/db/migrations/0015_webhook_tables.sql +41 -0
- package/lib/db/migrations/0016_github_integration.sql +30 -0
- package/lib/db/migrations/0016_overjoyed_ghost_rider.sql +22 -0
- package/lib/db/migrations/0017_slimy_inhumans.sql +6 -0
- package/lib/db/migrations/0018_same_spitfire.sql +1 -0
- package/lib/db/migrations/0019_jittery_loners.sql +16 -0
- package/lib/db/migrations/0019_remove_projects_add_org_settings.sql +14 -0
- package/lib/db/migrations/meta/0000_snapshot.json +374 -0
- package/lib/db/migrations/meta/0001_snapshot.json +553 -0
- package/lib/db/migrations/meta/0002_snapshot.json +560 -0
- package/lib/db/migrations/meta/0003_snapshot.json +650 -0
- package/lib/db/migrations/meta/0004_snapshot.json +852 -0
- package/lib/db/migrations/meta/0005_snapshot.json +900 -0
- package/lib/db/migrations/meta/0006_snapshot.json +1011 -0
- package/lib/db/migrations/meta/0007_snapshot.json +1125 -0
- package/lib/db/migrations/meta/0008_snapshot.json +1146 -0
- package/lib/db/migrations/meta/0009_snapshot.json +1386 -0
- package/lib/db/migrations/meta/0010_snapshot.json +1419 -0
- package/lib/db/migrations/meta/0011_snapshot.json +1615 -0
- package/lib/db/migrations/meta/0012_snapshot.json +1805 -0
- package/lib/db/migrations/meta/0013_snapshot.json +1948 -0
- package/lib/db/migrations/meta/0014_snapshot.json +2082 -0
- package/lib/db/migrations/meta/0015_snapshot.json +2476 -0
- package/lib/db/migrations/meta/0016_snapshot.json +2633 -0
- package/lib/db/migrations/meta/0017_snapshot.json +2680 -0
- package/lib/db/migrations/meta/0018_snapshot.json +2686 -0
- package/lib/db/migrations/meta/0019_snapshot.json +2741 -0
- package/lib/db/migrations/meta/_journal.json +146 -0
- package/lib/db/schema/ai-processing.ts +90 -0
- package/lib/db/schema/api-keys.ts +61 -0
- package/lib/db/schema/attachments.ts +48 -0
- package/lib/db/schema/auth.ts +111 -0
- package/lib/db/schema/comments.ts +74 -0
- package/lib/db/schema/duplicates.ts +80 -0
- package/lib/db/schema/feedback.ts +88 -0
- package/lib/db/schema/github-integrations.ts +66 -0
- package/lib/db/schema/index.ts +35 -0
- package/lib/db/schema/invitations.ts +32 -0
- package/lib/db/schema/notifications.ts +85 -0
- package/lib/db/schema/organization-members.ts +37 -0
- package/lib/db/schema/organization-settings.ts +134 -0
- package/lib/db/schema/organizations.ts +30 -0
- package/lib/db/schema/projects.ts +145 -0
- package/lib/db/schema/status-history.ts +63 -0
- package/lib/db/schema/tags.ts +194 -0
- package/lib/db/schema/user-profiles.ts +31 -0
- package/lib/db/schema/votes.ts +60 -0
- package/lib/db/schema/webhooks.ts +106 -0
- package/lib/feedback/filters.ts +28 -0
- package/lib/feedback/find-similar.ts +49 -0
- package/lib/feedback/get-feedback-by-id.ts +159 -0
- package/lib/feedback/prefill.ts +51 -0
- package/lib/http/get-request-url.ts +28 -0
- package/lib/integrations/github.ts +159 -0
- package/lib/invitations.ts +22 -0
- package/lib/logger.test.ts +31 -0
- package/lib/logger.ts +58 -0
- package/lib/middleware/api-key.ts +126 -0
- package/lib/middleware/rate-limit-keys.ts +47 -0
- package/lib/middleware/rate-limit.ts +148 -0
- package/lib/middleware/rbac.ts +39 -0
- package/lib/middleware/request-id.test.ts +28 -0
- package/lib/middleware/request-id.ts +30 -0
- package/lib/middleware/request-logger.test.ts +36 -0
- package/lib/middleware/request-logger.ts +41 -0
- package/lib/middleware/with-rate-limit.ts +33 -0
- package/lib/portal/analytics.ts +20 -0
- package/lib/portal/contributors.ts +27 -0
- package/lib/portal/i18n.ts +20 -0
- package/lib/portal/leaderboard-settings.ts +20 -0
- package/lib/portal/modules.ts +20 -0
- package/lib/portal/portal-copy.ts +20 -0
- package/lib/portal/public-context.tsx +110 -0
- package/lib/portal/seo.ts +20 -0
- package/lib/portal/settings-context.ts +56 -0
- package/lib/portal/sharing.ts +20 -0
- package/lib/portal/sorting.ts +20 -0
- package/lib/portal/theme.ts +20 -0
- package/lib/services/ai/classifier.ts +296 -0
- package/lib/services/ai/duplicate-detector.ts +255 -0
- package/lib/services/ai/tag-suggester.ts +108 -0
- package/lib/services/api-keys.ts +164 -0
- package/lib/services/backup.ts +173 -0
- package/lib/services/email/templates.ts +158 -0
- package/lib/services/email.ts +68 -0
- package/lib/services/github-sync.ts +205 -0
- package/lib/services/notifications/index.ts +224 -0
- package/lib/services/portal-settings.ts +157 -0
- package/lib/swagger/config.ts +296 -0
- package/lib/swagger/generate.ts +400 -0
- package/lib/upload/file-validator.ts +52 -0
- package/lib/upload/storage.ts +59 -0
- package/lib/utils/format.ts +26 -0
- package/lib/utils/slug.ts +28 -0
- package/lib/utils.ts +23 -0
- package/lib/validations/auth.ts +56 -0
- package/lib/validations/comment.ts +44 -0
- package/lib/validations/feedback.ts +51 -0
- package/lib/validations/invitations.ts +23 -0
- package/lib/validations/organizations.ts +34 -0
- package/lib/validations/projects.ts +49 -0
- package/lib/validators/feedback.ts +57 -0
- package/lib/validators/index.ts +18 -0
- package/lib/webhooks/events.ts +73 -0
- package/lib/webhooks/index.ts +21 -0
- package/lib/webhooks/retry.ts +188 -0
- package/lib/webhooks/sender.ts +183 -0
- package/lib/webhooks/verify.ts +37 -0
- package/lib/workers/feedback-processor.ts +255 -0
- package/messages/en.json +965 -0
- package/messages/jp.json +862 -0
- package/messages/zh-CN.json +855 -0
- package/next-env.d.ts +6 -0
- package/next.config.ts +66 -0
- package/package.json +84 -0
- package/playwright.config.ts +44 -0
- package/postcss.config.mjs +7 -0
- package/proxy.test.ts +131 -0
- package/proxy.ts +190 -0
- package/public/file.svg +1 -0
- package/public/globe.svg +1 -0
- package/public/logo-64.svg +5 -0
- package/public/logo.svg +5 -0
- package/public/next.svg +1 -0
- package/public/openapi.json +673 -0
- package/public/uploads/.gitkeep +0 -0
- package/public/uploads/02695701-ded0-4c81-8a21-9326c1d65448.pdf +1 -0
- package/public/uploads/178843ea-2780-48ef-8988-f4cba442e4cb.pdf +1 -0
- package/public/uploads/24b0a9ef-da93-49da-934f-637f89c7871d.pdf +1 -0
- package/public/uploads/7a11626d-a8e4-4b91-a8eb-20b6213b0a5a.pdf +1 -0
- package/public/uploads/b0703f4d-6e7b-4aab-8191-1a7b15f1b8ee.pdf +1 -0
- package/public/uploads/c8de0aed-4d3a-44aa-83bb-6594b7a2ddb3.pdf +1 -0
- package/public/uploads/e4cce295-0d85-4525-a1b0-a61c45722e26.pdf +1 -0
- package/public/uploads/eb4df45e-563c-48b8-9c68-c18212312426.pdf +1 -0
- package/public/vercel.svg +1 -0
- package/public/widget/embed.js +249 -0
- package/public/window.svg +1 -0
- package/scripts/backup-db.sh +57 -0
- package/scripts/backup-db.ts +24 -0
- package/scripts/generate-openapi.ts +22 -0
- package/scripts/migration-helper.ts +39 -0
- package/scripts/pre-deploy.ts +75 -0
- package/scripts/restore-db.sh +60 -0
- package/scripts/rollback.ts +72 -0
- package/scripts/seed-tags.ts +48 -0
- package/tests/api/feedback-bulk.test.ts +47 -0
- package/tests/api/feedback-by-id.test.ts +67 -0
- package/tests/api/feedback-comments-route-import.test.ts +26 -0
- package/tests/api/feedback-create.test.ts +71 -0
- package/tests/api/feedback-delete.test.ts +160 -0
- package/tests/api/feedback-filter.test.ts +250 -0
- package/tests/api/feedback-list.test.ts +234 -0
- package/tests/api/feedback-route-assignee-condition.test.ts +32 -0
- package/tests/api/feedback-similar.test.ts +46 -0
- package/tests/api/feedback-sort.test.ts +261 -0
- package/tests/api/feedback-status-enum.test.ts +49 -0
- package/tests/api/feedback-status-filter.test.ts +117 -0
- package/tests/api/feedback-submit-on-behalf.test.ts +269 -0
- package/tests/api/feedback.test.ts +175 -0
- package/tests/api/identify-jwt.test.ts +25 -0
- package/tests/api/invitation-accept.test.ts +213 -0
- package/tests/api/organization-invitations.test.ts +186 -0
- package/tests/api/organization-members-list.test.ts +79 -0
- package/tests/api/organization-members.test.ts +340 -0
- package/tests/api/organizations.test.ts +149 -0
- package/tests/api/register.test.ts +112 -0
- package/tests/api/upload.test.ts +103 -0
- package/tests/api/vote.test.ts +82 -0
- package/tests/app/admin-feedback-detail-page.test.tsx +25 -0
- package/tests/app/admin-feedback-list-page.test.tsx +25 -0
- package/tests/app/admin-feedback-new-page.test.tsx +25 -0
- package/tests/app/health-route-helpers.test.ts +27 -0
- package/tests/app/login-page.test.ts +26 -0
- package/tests/app/portal-page.test.ts +29 -0
- package/tests/app/project-portal-overview.test.tsx +25 -0
- package/tests/app/widget-page-import.test.ts +25 -0
- package/tests/components/create-post-dialog-defaults.test.ts +43 -0
- package/tests/components/feedback/duplicate-suggestions-inline.test.tsx +27 -0
- package/tests/components/feedback/embedded-feedback-form.test.tsx +96 -0
- package/tests/components/feedback/feedback-detail.test.tsx +25 -0
- package/tests/components/feedback/feedback-stats.test.tsx +49 -0
- package/tests/components/feedback-bulk-actions.test.tsx +39 -0
- package/tests/components/feedback-i18n-keys.test.ts +70 -0
- package/tests/components/feedback-list-controls-compile.test.ts +25 -0
- package/tests/components/feedback-list-controls.test.tsx +204 -0
- package/tests/components/feedback-list-item.test.tsx +67 -0
- package/tests/components/landing/hero.test.tsx +46 -0
- package/tests/components/layout/language-switcher.test.tsx +25 -0
- package/tests/components/layout/sidebar.test.tsx +157 -0
- package/tests/components/login-form.test.ts +25 -0
- package/tests/components/organization-form.test.ts +32 -0
- package/tests/components/organization-switcher.test.ts +25 -0
- package/tests/components/pagination.test.tsx +43 -0
- package/tests/components/portal-overview.test.tsx +25 -0
- package/tests/components/profile-form.test.tsx +139 -0
- package/tests/components/role-selector.test.ts +31 -0
- package/tests/components/status-chart.test.tsx +90 -0
- package/tests/e2e/auth.e2e.ts +323 -0
- package/tests/e2e/feedback-actions.e2e.ts +471 -0
- package/tests/e2e/feedback-attachment.e2e.ts +168 -0
- package/tests/e2e/feedback-customer.e2e.ts +226 -0
- package/tests/e2e/feedback-management.e2e.ts +565 -0
- package/tests/e2e/feedback-submit.e2e.ts +133 -0
- package/tests/e2e/feedback-view.e2e.ts +297 -0
- package/tests/e2e/fixtures/test-data.ts +235 -0
- package/tests/e2e/health-check.e2e.ts +230 -0
- package/tests/e2e/helpers/test-utils-helpers.test.ts +43 -0
- package/tests/e2e/helpers/test-utils.ts +298 -0
- package/tests/e2e/integration-placeholders.e2e.ts +199 -0
- package/tests/e2e/organization.e2e.ts +292 -0
- package/tests/e2e/permissions.e2e.ts +424 -0
- package/tests/e2e/project-widget.e2e.ts +63 -0
- package/tests/feedback/filters.test.ts +29 -0
- package/tests/hooks/use-permissions.test.ts +52 -0
- package/tests/lib/ai/classifier.test.ts +104 -0
- package/tests/lib/ai/duplicate-detector.test.ts +234 -0
- package/tests/lib/attachments-schema.test.ts +30 -0
- package/tests/lib/auth/session.test.ts +49 -0
- package/tests/lib/auth-client.test.ts +37 -0
- package/tests/lib/auth-config.test.ts +26 -0
- package/tests/lib/feedback-prefill.test.ts +52 -0
- package/tests/lib/feedback-processor.test.ts +41 -0
- package/tests/lib/feedback-schema.test.ts +33 -0
- package/tests/lib/file-validator.test.ts +48 -0
- package/tests/lib/get-feedback-by-id.test.ts +37 -0
- package/tests/lib/invitations.test.ts +35 -0
- package/tests/lib/login-schema.test.ts +36 -0
- package/tests/lib/org-context.test.ts +95 -0
- package/tests/lib/organization-access.test.ts +44 -0
- package/tests/lib/organization-member-role-schema.test.ts +41 -0
- package/tests/lib/permissions.test.ts +88 -0
- package/tests/lib/portal-analytics.test.ts +25 -0
- package/tests/lib/portal-contributors.test.ts +25 -0
- package/tests/lib/portal-copy.test.ts +27 -0
- package/tests/lib/portal-i18n.test.ts +30 -0
- package/tests/lib/portal-leaderboard-settings.test.ts +25 -0
- package/tests/lib/portal-modules.test.ts +25 -0
- package/tests/lib/portal-seo.test.ts +25 -0
- package/tests/lib/portal-sharing.test.ts +25 -0
- package/tests/lib/portal-sorting.test.ts +25 -0
- package/tests/lib/portal-theme.test.ts +25 -0
- package/tests/lib/rate-limit.test.ts +142 -0
- package/tests/lib/resolve-locale.test.ts +34 -0
- package/tests/lib/services/backup.test.ts +145 -0
- package/tests/lib/user-organizations.test.ts +42 -0
- package/tests/lib/user-role-schema.test.ts +33 -0
- package/tests/lib/user-schema.test.ts +25 -0
- package/tests/setup.ts +74 -0
- package/tsconfig.json +34 -0
- package/types/bun-test.d.ts +31 -0
|
@@ -0,0 +1,109 @@
|
|
|
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 { notFound } from "next/navigation";
|
|
19
|
+
import { PortalLayout } from "@/components/portal/portal-layout";
|
|
20
|
+
import { ChangelogList } from "@/components/portal/changelog-list";
|
|
21
|
+
import type { ChangelogItem } from "@/components/portal/changelog-entry";
|
|
22
|
+
import { Button } from "@/components/ui/button";
|
|
23
|
+
import { getPortalPublicContext, getPortalSections } from "@/lib/portal/public-context";
|
|
24
|
+
import { getTranslations } from "next-intl/server";
|
|
25
|
+
|
|
26
|
+
type PageProps = {
|
|
27
|
+
params: Promise<{ organizationSlug: string }>;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
31
|
+
async function getChangelogEntries(_organizationId: string): Promise<ChangelogItem[]> {
|
|
32
|
+
// TODO: Implement actual changelog query from database
|
|
33
|
+
// For now, return sample data
|
|
34
|
+
return [
|
|
35
|
+
{
|
|
36
|
+
id: "1",
|
|
37
|
+
title: "New Feedback Portal Design",
|
|
38
|
+
content: "<p>We've completely redesigned the feedback portal with a modern, clean interface. The new design features improved navigation, better organization of content, and enhanced readability.</p>",
|
|
39
|
+
type: "feature",
|
|
40
|
+
publishedAt: new Date().toISOString(),
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
id: "2",
|
|
44
|
+
title: "Roadmap Kanban View",
|
|
45
|
+
content: "<p>You can now view the product roadmap in a beautiful Kanban-style board with columns for Backlog, Next Up, In Progress, and Done.</p>",
|
|
46
|
+
type: "feature",
|
|
47
|
+
publishedAt: new Date(Date.now() - 7 * 24 * 60 * 60 * 1000).toISOString(),
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
id: "3",
|
|
51
|
+
title: "Performance Improvements",
|
|
52
|
+
content: "<p>Significant performance improvements across the entire portal. Pages now load 40% faster.</p>",
|
|
53
|
+
type: "improvement",
|
|
54
|
+
publishedAt: new Date(Date.now() - 14 * 24 * 60 * 60 * 1000).toISOString(),
|
|
55
|
+
},
|
|
56
|
+
];
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export async function generateMetadata({ params }: PageProps) {
|
|
60
|
+
const { organizationSlug } = await params;
|
|
61
|
+
const context = await getPortalPublicContext(organizationSlug);
|
|
62
|
+
const organization = context?.organization;
|
|
63
|
+
|
|
64
|
+
if (!organization || !context?.portalEnabled) {
|
|
65
|
+
return { title: "Not Found" };
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return {
|
|
69
|
+
title: `Changelog - ${organization.name}`,
|
|
70
|
+
description: `Latest updates and announcements from ${organization.name}`,
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export default async function OrganizationChangelogPage({ params }: PageProps) {
|
|
75
|
+
const t = await getTranslations("portal.changelog");
|
|
76
|
+
const { organizationSlug } = await params;
|
|
77
|
+
|
|
78
|
+
const context = await getPortalPublicContext(organizationSlug);
|
|
79
|
+
const organization = context?.organization;
|
|
80
|
+
|
|
81
|
+
if (!organization || !context?.portalEnabled || !context.modules.changelog) {
|
|
82
|
+
notFound();
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const entries = await getChangelogEntries(organization.id);
|
|
86
|
+
const sections = getPortalSections(organizationSlug, context.modules);
|
|
87
|
+
|
|
88
|
+
return (
|
|
89
|
+
<PortalLayout
|
|
90
|
+
organizationName={organization.name}
|
|
91
|
+
organizationSlug={organizationSlug}
|
|
92
|
+
sections={sections}
|
|
93
|
+
>
|
|
94
|
+
<div className="space-y-8">
|
|
95
|
+
<div className="flex flex-col gap-4 sm:flex-row sm:items-end sm:justify-between">
|
|
96
|
+
<div>
|
|
97
|
+
<h1 className="text-3xl font-bold tracking-tight">{t("title")}</h1>
|
|
98
|
+
<p className="text-muted-foreground mt-2">
|
|
99
|
+
Stay up to date with the latest updates and improvements.
|
|
100
|
+
</p>
|
|
101
|
+
</div>
|
|
102
|
+
<Button size="lg">{t("subscribeToUpdates")}</Button>
|
|
103
|
+
</div>
|
|
104
|
+
|
|
105
|
+
<ChangelogList entries={entries} />
|
|
106
|
+
</div>
|
|
107
|
+
</PortalLayout>
|
|
108
|
+
);
|
|
109
|
+
}
|
|
@@ -0,0 +1,146 @@
|
|
|
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 Link from "next/link";
|
|
19
|
+
import { notFound } from "next/navigation";
|
|
20
|
+
import { headers } from "next/headers";
|
|
21
|
+
import { getTranslations } from "next-intl/server";
|
|
22
|
+
import { PortalLayout } from "@/components/portal/portal-layout";
|
|
23
|
+
import { FeedbackDetail } from "@/components/feedback/feedback-detail";
|
|
24
|
+
import { PublicComments } from "@/components/comment/public-comments";
|
|
25
|
+
import { Button } from "@/components/ui/button";
|
|
26
|
+
import { getPortalPublicContext, getPortalSections } from "@/lib/portal/public-context";
|
|
27
|
+
import { getFeedbackById } from "@/lib/feedback/get-feedback-by-id";
|
|
28
|
+
import { auth } from "@/lib/auth/config";
|
|
29
|
+
|
|
30
|
+
export const dynamic = "force-dynamic";
|
|
31
|
+
|
|
32
|
+
interface PageProps {
|
|
33
|
+
params: Promise<{ organizationSlug: string; id: string }>;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export async function generateMetadata({ params }: PageProps) {
|
|
37
|
+
const t = await getTranslations("feedback.detail");
|
|
38
|
+
const { organizationSlug, id } = await params;
|
|
39
|
+
const context = await getPortalPublicContext(organizationSlug);
|
|
40
|
+
const organization = context?.organization;
|
|
41
|
+
|
|
42
|
+
if (!organization || !context?.portalEnabled) {
|
|
43
|
+
return { title: t("publicTitle") };
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const feedbackId = Number(id);
|
|
47
|
+
if (Number.isNaN(feedbackId)) {
|
|
48
|
+
return { title: t("publicTitle") };
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const { db } = await import("@/lib/db");
|
|
52
|
+
if (!db) {
|
|
53
|
+
return { title: t("publicTitle") };
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const result = await getFeedbackById(db, feedbackId);
|
|
57
|
+
if (!result || "deleted" in result) {
|
|
58
|
+
return { title: t("publicTitle") };
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (result.feedback.organizationId !== organization.id) {
|
|
62
|
+
return { title: t("publicTitle") };
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return {
|
|
66
|
+
title: t("publicTitle"),
|
|
67
|
+
description: t("publicDescription"),
|
|
68
|
+
robots: context.seo?.noIndex ? { index: false, follow: false } : undefined,
|
|
69
|
+
openGraph: {
|
|
70
|
+
title: t("publicTitle"),
|
|
71
|
+
description: t("publicDescription"),
|
|
72
|
+
type: "website",
|
|
73
|
+
},
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export default async function OrganizationFeedbackDetailPage({ params }: PageProps) {
|
|
78
|
+
const t = await getTranslations("feedback.detail");
|
|
79
|
+
const { organizationSlug, id } = await params;
|
|
80
|
+
const feedbackId = Number(id);
|
|
81
|
+
|
|
82
|
+
if (Number.isNaN(feedbackId)) {
|
|
83
|
+
notFound();
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const context = await getPortalPublicContext(organizationSlug);
|
|
87
|
+
const organization = context?.organization;
|
|
88
|
+
|
|
89
|
+
if (!organization || !context?.portalEnabled || !context.modules.feedback) {
|
|
90
|
+
notFound();
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const { db } = await import("@/lib/db");
|
|
94
|
+
if (!db) {
|
|
95
|
+
notFound();
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const result = await getFeedbackById(db, feedbackId);
|
|
99
|
+
if (!result || "deleted" in result) {
|
|
100
|
+
notFound();
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (result.feedback.organizationId !== organization.id) {
|
|
104
|
+
notFound();
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const session = await auth.api.getSession({ headers: await headers() });
|
|
108
|
+
const isAuthenticated = !!session?.user;
|
|
109
|
+
|
|
110
|
+
const sections = getPortalSections(organizationSlug, context.modules);
|
|
111
|
+
|
|
112
|
+
return (
|
|
113
|
+
<PortalLayout
|
|
114
|
+
organizationName={organization.name}
|
|
115
|
+
organizationSlug={organizationSlug}
|
|
116
|
+
sections={sections}
|
|
117
|
+
>
|
|
118
|
+
<div className="mx-auto max-w-3xl space-y-6">
|
|
119
|
+
<div className="flex flex-wrap items-center justify-between gap-3">
|
|
120
|
+
<h2 className="text-sm text-muted-foreground">{t("publicHeader")}</h2>
|
|
121
|
+
<div className="flex flex-wrap gap-2">
|
|
122
|
+
<Button asChild variant="outline" size="sm">
|
|
123
|
+
<Link href={`/${organizationSlug}`}>{t("publicBackHome")}</Link>
|
|
124
|
+
</Button>
|
|
125
|
+
<Button asChild size="sm">
|
|
126
|
+
<Link href={`/${organizationSlug}`}>{t("publicContinueSubmit")}</Link>
|
|
127
|
+
</Button>
|
|
128
|
+
</div>
|
|
129
|
+
</div>
|
|
130
|
+
<FeedbackDetail
|
|
131
|
+
feedback={{
|
|
132
|
+
...result.feedback,
|
|
133
|
+
attachments: [],
|
|
134
|
+
}}
|
|
135
|
+
/>
|
|
136
|
+
<PublicComments
|
|
137
|
+
feedbackId={feedbackId}
|
|
138
|
+
isAuthenticated={isAuthenticated}
|
|
139
|
+
organizationId={organization.id}
|
|
140
|
+
allowPublicComments={context.sharing.allowPublicComments}
|
|
141
|
+
className="mt-6"
|
|
142
|
+
/>
|
|
143
|
+
</div>
|
|
144
|
+
</PortalLayout>
|
|
145
|
+
);
|
|
146
|
+
}
|
|
@@ -0,0 +1,160 @@
|
|
|
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 { notFound } from "next/navigation";
|
|
19
|
+
import { headers } from "next/headers";
|
|
20
|
+
import { and, desc, eq, isNull, sql } from "drizzle-orm";
|
|
21
|
+
import { PortalLayout } from "@/components/portal/portal-layout";
|
|
22
|
+
import { FeedbackBoard } from "@/components/portal/feedback-board";
|
|
23
|
+
import type { FeedbackPost } from "@/components/portal/feedback-post-card";
|
|
24
|
+
import type { Contributor } from "@/components/portal/contributors-sidebar";
|
|
25
|
+
import { getPortalPublicContext, getPortalSections } from "@/lib/portal/public-context";
|
|
26
|
+
import { auth } from "@/lib/auth/config";
|
|
27
|
+
|
|
28
|
+
type PageProps = {
|
|
29
|
+
params: Promise<{ organizationSlug: string }>;
|
|
30
|
+
searchParams: Promise<{ sort?: string }>;
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
async function getFeedbackPosts(
|
|
34
|
+
organizationId: string,
|
|
35
|
+
sort: string = "new"
|
|
36
|
+
): Promise<FeedbackPost[]> {
|
|
37
|
+
const { db } = await import("@/lib/db");
|
|
38
|
+
if (!db) return [];
|
|
39
|
+
|
|
40
|
+
// Import feedback schema
|
|
41
|
+
const { feedback } = await import("@/lib/db/schema");
|
|
42
|
+
|
|
43
|
+
const voteCountSubquery = sql<number>`(
|
|
44
|
+
SELECT COUNT(*)::int
|
|
45
|
+
FROM "votes"
|
|
46
|
+
WHERE "votes"."feedbackId" = "feedback"."feedbackId"
|
|
47
|
+
)`;
|
|
48
|
+
|
|
49
|
+
// Count only public comments (isInternal = false)
|
|
50
|
+
const commentCountSubquery = sql<number>`(
|
|
51
|
+
SELECT COUNT(*)::int
|
|
52
|
+
FROM "comments"
|
|
53
|
+
WHERE "comments"."feedbackId" = "feedback"."feedbackId"
|
|
54
|
+
AND "comments"."isInternal" = false
|
|
55
|
+
)`;
|
|
56
|
+
|
|
57
|
+
const orderByClause =
|
|
58
|
+
sort === "top"
|
|
59
|
+
? desc(voteCountSubquery)
|
|
60
|
+
: sort === "trending"
|
|
61
|
+
? desc(voteCountSubquery) // Simplified: could add time weight
|
|
62
|
+
: desc(feedback.createdAt);
|
|
63
|
+
|
|
64
|
+
const results = await db
|
|
65
|
+
.select({
|
|
66
|
+
id: feedback.feedbackId,
|
|
67
|
+
title: feedback.title,
|
|
68
|
+
description: feedback.description,
|
|
69
|
+
type: feedback.type,
|
|
70
|
+
status: feedback.status,
|
|
71
|
+
voteCount: voteCountSubquery,
|
|
72
|
+
commentCount: commentCountSubquery,
|
|
73
|
+
createdAt: feedback.createdAt,
|
|
74
|
+
})
|
|
75
|
+
.from(feedback)
|
|
76
|
+
.where(
|
|
77
|
+
and(
|
|
78
|
+
eq(feedback.organizationId, organizationId),
|
|
79
|
+
isNull(feedback.deletedAt),
|
|
80
|
+
),
|
|
81
|
+
)
|
|
82
|
+
.orderBy(orderByClause)
|
|
83
|
+
.limit(50);
|
|
84
|
+
|
|
85
|
+
return results.map((r) => ({
|
|
86
|
+
id: r.id,
|
|
87
|
+
title: r.title,
|
|
88
|
+
description: r.description,
|
|
89
|
+
type: r.type,
|
|
90
|
+
status: r.status,
|
|
91
|
+
voteCount: r.voteCount ?? 0,
|
|
92
|
+
commentCount: r.commentCount ?? 0,
|
|
93
|
+
createdAt: r.createdAt?.toISOString() ?? new Date().toISOString(),
|
|
94
|
+
author: null, // TODO: Add author info
|
|
95
|
+
}));
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
99
|
+
async function getTopContributors(_organizationId: string): Promise<Contributor[]> {
|
|
100
|
+
// TODO: Implement actual contributor query
|
|
101
|
+
// For now, return empty array
|
|
102
|
+
return [];
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export async function generateMetadata({ params }: PageProps) {
|
|
106
|
+
const { organizationSlug } = await params;
|
|
107
|
+
const context = await getPortalPublicContext(organizationSlug);
|
|
108
|
+
const organization = context?.organization;
|
|
109
|
+
|
|
110
|
+
if (!organization || !context?.portalEnabled) {
|
|
111
|
+
return { title: "Not Found" };
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
return {
|
|
115
|
+
title: `${organization.name} - Feedback Portal`,
|
|
116
|
+
description: organization.description || `${organization.name} feedback portal`,
|
|
117
|
+
robots: context.seo.noIndex ? { index: false, follow: false } : undefined,
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export default async function OrganizationPortalPage({ params, searchParams }: PageProps) {
|
|
122
|
+
const { organizationSlug } = await params;
|
|
123
|
+
const { sort = "new" } = await searchParams;
|
|
124
|
+
|
|
125
|
+
const context = await getPortalPublicContext(organizationSlug);
|
|
126
|
+
const organization = context?.organization;
|
|
127
|
+
|
|
128
|
+
if (!organization || !context?.portalEnabled || !context.modules.feedback) {
|
|
129
|
+
notFound();
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const [posts, contributors] = await Promise.all([
|
|
133
|
+
getFeedbackPosts(organization.id, sort),
|
|
134
|
+
getTopContributors(organization.id),
|
|
135
|
+
]);
|
|
136
|
+
|
|
137
|
+
const sections = getPortalSections(organizationSlug, context.modules);
|
|
138
|
+
const session = await auth.api.getSession({ headers: await headers() });
|
|
139
|
+
const isAuthenticated = !!session?.user;
|
|
140
|
+
|
|
141
|
+
return (
|
|
142
|
+
<PortalLayout
|
|
143
|
+
organizationName={organization.name}
|
|
144
|
+
organizationSlug={organizationSlug}
|
|
145
|
+
sections={sections}
|
|
146
|
+
>
|
|
147
|
+
<FeedbackBoard
|
|
148
|
+
organizationId={organization.id}
|
|
149
|
+
organizationSlug={organizationSlug}
|
|
150
|
+
posts={posts}
|
|
151
|
+
isAuthenticated={isAuthenticated}
|
|
152
|
+
contributors={contributors}
|
|
153
|
+
totalCount={posts.length}
|
|
154
|
+
showVoteCount={context.sharing.showVoteCount}
|
|
155
|
+
showAuthor={context.sharing.showAuthor}
|
|
156
|
+
allowPublicVoting={context.sharing.allowPublicVoting}
|
|
157
|
+
/>
|
|
158
|
+
</PortalLayout>
|
|
159
|
+
);
|
|
160
|
+
}
|
|
@@ -0,0 +1,142 @@
|
|
|
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 Link from "next/link";
|
|
19
|
+
import { notFound } from "next/navigation";
|
|
20
|
+
import { and, desc, eq, isNull, sql } from "drizzle-orm";
|
|
21
|
+
import { headers } from "next/headers";
|
|
22
|
+
import { auth } from "@/lib/auth/config";
|
|
23
|
+
import { PortalLayout } from "@/components/portal/portal-layout";
|
|
24
|
+
import { RoadmapBoard, type RoadmapStatus } from "@/components/portal/roadmap-board";
|
|
25
|
+
import type { RoadmapItem } from "@/components/portal/roadmap-column";
|
|
26
|
+
import { Button } from "@/components/ui/button";
|
|
27
|
+
import { getPortalPublicContext, getPortalSections } from "@/lib/portal/public-context";
|
|
28
|
+
import { getTranslations } from "next-intl/server";
|
|
29
|
+
|
|
30
|
+
type PageProps = {
|
|
31
|
+
params: Promise<{ organizationSlug: string }>;
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
async function getRoadmapItems(organizationId: string): Promise<RoadmapItem[]> {
|
|
35
|
+
const { db } = await import("@/lib/db");
|
|
36
|
+
if (!db) return [];
|
|
37
|
+
|
|
38
|
+
// Import feedback schema and get items with roadmap-related statuses
|
|
39
|
+
const { feedback } = await import("@/lib/db/schema");
|
|
40
|
+
|
|
41
|
+
// Map feedback statuses to roadmap statuses
|
|
42
|
+
const statusMap: Record<string, RoadmapStatus> = {
|
|
43
|
+
new: "backlog",
|
|
44
|
+
planned: "next-up",
|
|
45
|
+
"in-progress": "in-progress",
|
|
46
|
+
completed: "done",
|
|
47
|
+
closed: "done",
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
const voteCountSubquery = sql<number>`(
|
|
51
|
+
SELECT COUNT(*)::int
|
|
52
|
+
FROM "votes"
|
|
53
|
+
WHERE "votes"."feedbackId" = "feedback"."feedbackId"
|
|
54
|
+
)`;
|
|
55
|
+
|
|
56
|
+
const results = await db
|
|
57
|
+
.select({
|
|
58
|
+
id: feedback.feedbackId,
|
|
59
|
+
title: feedback.title,
|
|
60
|
+
type: feedback.type,
|
|
61
|
+
status: feedback.status,
|
|
62
|
+
voteCount: voteCountSubquery,
|
|
63
|
+
})
|
|
64
|
+
.from(feedback)
|
|
65
|
+
.where(
|
|
66
|
+
and(
|
|
67
|
+
eq(feedback.organizationId, organizationId),
|
|
68
|
+
isNull(feedback.deletedAt),
|
|
69
|
+
),
|
|
70
|
+
)
|
|
71
|
+
.orderBy(desc(voteCountSubquery))
|
|
72
|
+
.limit(100);
|
|
73
|
+
|
|
74
|
+
return results.map((r) => ({
|
|
75
|
+
id: r.id,
|
|
76
|
+
title: r.title,
|
|
77
|
+
category: r.type,
|
|
78
|
+
voteCount: r.voteCount ?? 0,
|
|
79
|
+
status: statusMap[r.status] ?? "backlog",
|
|
80
|
+
}));
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export async function generateMetadata({ params }: PageProps) {
|
|
84
|
+
const { organizationSlug } = await params;
|
|
85
|
+
const context = await getPortalPublicContext(organizationSlug);
|
|
86
|
+
const organization = context?.organization;
|
|
87
|
+
|
|
88
|
+
if (!organization || !context?.portalEnabled) {
|
|
89
|
+
return { title: "Not Found" };
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return {
|
|
93
|
+
title: `Roadmap - ${organization.name}`,
|
|
94
|
+
description: `View the product roadmap for ${organization.name}`,
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export default async function OrganizationRoadmapPage({ params }: PageProps) {
|
|
99
|
+
const t = await getTranslations("portal.roadmap");
|
|
100
|
+
const { organizationSlug } = await params;
|
|
101
|
+
|
|
102
|
+
const context = await getPortalPublicContext(organizationSlug);
|
|
103
|
+
const organization = context?.organization;
|
|
104
|
+
|
|
105
|
+
if (!organization || !context?.portalEnabled || !context.modules.roadmap) {
|
|
106
|
+
notFound();
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Check if user is admin
|
|
110
|
+
const session = await auth.api.getSession({ headers: await headers() });
|
|
111
|
+
const isAdmin = session?.user?.role === "admin" || session?.user?.role === "owner";
|
|
112
|
+
|
|
113
|
+
const items = await getRoadmapItems(organization.id);
|
|
114
|
+
const sections = getPortalSections(organizationSlug, context.modules);
|
|
115
|
+
|
|
116
|
+
return (
|
|
117
|
+
<PortalLayout
|
|
118
|
+
organizationName={organization.name}
|
|
119
|
+
organizationSlug={organizationSlug}
|
|
120
|
+
sections={sections}
|
|
121
|
+
>
|
|
122
|
+
<div className="space-y-6">
|
|
123
|
+
<div className="flex flex-col gap-4 sm:flex-row sm:items-end sm:justify-between">
|
|
124
|
+
<div>
|
|
125
|
+
<h1 className="text-3xl font-bold tracking-tight">{t("title")}</h1>
|
|
126
|
+
<p className="text-muted-foreground mt-2">
|
|
127
|
+
See what we're working on and what's coming next.
|
|
128
|
+
</p>
|
|
129
|
+
</div>
|
|
130
|
+
<div className="flex flex-col gap-2 sm:flex-row">
|
|
131
|
+
<Button size="lg">{t("subscribeToUpdates")}</Button>
|
|
132
|
+
<Button variant="secondary" size="lg" asChild>
|
|
133
|
+
<Link href={`/${organizationSlug}`}>{t("submitFeedback")}</Link>
|
|
134
|
+
</Button>
|
|
135
|
+
</div>
|
|
136
|
+
</div>
|
|
137
|
+
|
|
138
|
+
<RoadmapBoard items={items} isAdmin={isAdmin} />
|
|
139
|
+
</div>
|
|
140
|
+
</PortalLayout>
|
|
141
|
+
);
|
|
142
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
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
|
+
'use client';
|
|
19
|
+
|
|
20
|
+
import dynamic from 'next/dynamic';
|
|
21
|
+
import 'swagger-ui-react/swagger-ui.css';
|
|
22
|
+
import { useEffect, useState } from 'react';
|
|
23
|
+
|
|
24
|
+
const SwaggerUI = dynamic(() => import('swagger-ui-react'), { ssr: false });
|
|
25
|
+
|
|
26
|
+
export default function ApiDocsPage() {
|
|
27
|
+
const [spec, setSpec] = useState<Record<string, unknown> | null>(null);
|
|
28
|
+
|
|
29
|
+
useEffect(() => {
|
|
30
|
+
fetch('/api/docs')
|
|
31
|
+
.then((res) => res.json())
|
|
32
|
+
.then(setSpec);
|
|
33
|
+
}, []);
|
|
34
|
+
|
|
35
|
+
if (!spec) {
|
|
36
|
+
return (
|
|
37
|
+
<div className="min-h-screen flex items-center justify-center">
|
|
38
|
+
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-primary" />
|
|
39
|
+
</div>
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return (
|
|
44
|
+
<div className="min-h-screen bg-background">
|
|
45
|
+
<SwaggerUI spec={spec} />
|
|
46
|
+
</div>
|
|
47
|
+
);
|
|
48
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
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 Link from "next/link";
|
|
19
|
+
import { Button } from "@/components/ui/button";
|
|
20
|
+
import { useTranslations } from "next-intl";
|
|
21
|
+
|
|
22
|
+
export default function NotFound() {
|
|
23
|
+
const t = useTranslations("errors.notFound");
|
|
24
|
+
return (
|
|
25
|
+
<div className="container mx-auto py-16 text-center">
|
|
26
|
+
<h1 className="text-4xl font-bold mb-4">{t("title")}</h1>
|
|
27
|
+
<p className="text-muted-foreground mb-8">该反馈不存在或已被删除。</p>
|
|
28
|
+
<Button asChild>
|
|
29
|
+
<Link href="/">返回首页</Link>
|
|
30
|
+
</Button>
|
|
31
|
+
</div>
|
|
32
|
+
);
|
|
33
|
+
}
|