@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,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
|
+
}
|