@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,174 @@
|
|
|
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 { getTranslations } from "next-intl/server";
|
|
20
|
+
|
|
21
|
+
import { Badge } from "@/components/ui/badge";
|
|
22
|
+
import { Button } from "@/components/ui/button";
|
|
23
|
+
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
|
24
|
+
import { PortalModulesPanel } from "@/components/portal/portal-modules-panel";
|
|
25
|
+
import { getDefaultPortalModules } from "@/lib/portal/modules";
|
|
26
|
+
import type { PortalConfig } from "@/lib/db/schema";
|
|
27
|
+
|
|
28
|
+
interface PortalOverviewProps {
|
|
29
|
+
portalLink: string;
|
|
30
|
+
portalConfig: PortalConfig | null;
|
|
31
|
+
organizationId: string;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export async function PortalOverview({
|
|
35
|
+
portalLink,
|
|
36
|
+
portalConfig,
|
|
37
|
+
organizationId,
|
|
38
|
+
}: PortalOverviewProps) {
|
|
39
|
+
const t = await getTranslations("settings.portal.overview");
|
|
40
|
+
|
|
41
|
+
const portalEnabled = portalConfig?.sharing?.enabled ?? true;
|
|
42
|
+
const themeSummary = portalConfig?.theme?.primaryColor
|
|
43
|
+
? t("themeSummary.custom", { color: portalConfig.theme.primaryColor })
|
|
44
|
+
: t("themeSummary.default");
|
|
45
|
+
const languageSummary = portalConfig?.defaultLanguage ?? "zh-CN";
|
|
46
|
+
const seoSummary = portalConfig?.seo?.metaTitle
|
|
47
|
+
? portalConfig.seo.metaTitle
|
|
48
|
+
: t("seoSummary.unset");
|
|
49
|
+
|
|
50
|
+
const modules = portalConfig?.modules ?? getDefaultPortalModules();
|
|
51
|
+
|
|
52
|
+
return (
|
|
53
|
+
<div className="space-y-8">
|
|
54
|
+
<Card className="border-slate-200/80 bg-white/80 shadow-sm">
|
|
55
|
+
<CardHeader className="flex flex-col gap-3 md:flex-row md:items-center md:justify-between">
|
|
56
|
+
<div>
|
|
57
|
+
<CardTitle className="text-xl">{t("status.title")}</CardTitle>
|
|
58
|
+
<CardDescription>{t("status.description")}</CardDescription>
|
|
59
|
+
</div>
|
|
60
|
+
<div className="flex flex-wrap items-center gap-3">
|
|
61
|
+
<Badge variant={portalEnabled ? "default" : "outline"}>
|
|
62
|
+
{portalEnabled ? t("status.badgeEnabled") : t("status.badgeDisabled")}
|
|
63
|
+
</Badge>
|
|
64
|
+
<Button asChild variant="outline" size="sm">
|
|
65
|
+
<Link href={portalLink}>{t("status.preview")}</Link>
|
|
66
|
+
</Button>
|
|
67
|
+
<Button asChild size="sm">
|
|
68
|
+
<Link href={portalLink}>{t("status.open")}</Link>
|
|
69
|
+
</Button>
|
|
70
|
+
</div>
|
|
71
|
+
</CardHeader>
|
|
72
|
+
<CardContent>
|
|
73
|
+
<div className="text-sm text-slate-600">
|
|
74
|
+
{t("status.portalLink")}{" "}
|
|
75
|
+
<span className="font-medium text-slate-900">{portalLink}</span>
|
|
76
|
+
</div>
|
|
77
|
+
</CardContent>
|
|
78
|
+
</Card>
|
|
79
|
+
|
|
80
|
+
<div className="grid gap-6 lg:grid-cols-3">
|
|
81
|
+
<PortalGroupCard
|
|
82
|
+
title={t("cards.experience.title")}
|
|
83
|
+
description={t("cards.experience.description")}
|
|
84
|
+
items={[
|
|
85
|
+
themeSummary,
|
|
86
|
+
portalConfig?.copy?.title
|
|
87
|
+
? t("cards.experience.items.copyTitle", { title: portalConfig.copy.title })
|
|
88
|
+
: t("cards.experience.items.copyTitleUnset"),
|
|
89
|
+
t("cards.experience.items.defaultLanguage", { locale: languageSummary }),
|
|
90
|
+
]}
|
|
91
|
+
href={`/settings/portal-branding`}
|
|
92
|
+
actionLabel={t("cards.action")}
|
|
93
|
+
/>
|
|
94
|
+
<PortalGroupCard
|
|
95
|
+
title={t("cards.growth.title")}
|
|
96
|
+
description={t("cards.growth.description")}
|
|
97
|
+
items={[
|
|
98
|
+
seoSummary,
|
|
99
|
+
portalConfig?.sharing?.socialSharing
|
|
100
|
+
? t("cards.growth.items.socialSharingEnabled")
|
|
101
|
+
: t("cards.growth.items.socialSharingDisabled"),
|
|
102
|
+
portalConfig?.seo?.ogImage
|
|
103
|
+
? t("cards.growth.items.ogImageSet")
|
|
104
|
+
: t("cards.growth.items.ogImageUnset"),
|
|
105
|
+
]}
|
|
106
|
+
href={`/settings/portal-growth`}
|
|
107
|
+
actionLabel={t("cards.action")}
|
|
108
|
+
/>
|
|
109
|
+
<PortalGroupCard
|
|
110
|
+
title={t("cards.visibility.title")}
|
|
111
|
+
description={t("cards.visibility.description")}
|
|
112
|
+
items={[
|
|
113
|
+
portalEnabled ? t("cards.visibility.items.portalEnabled") : t("cards.visibility.items.portalDisabled"),
|
|
114
|
+
portalConfig?.sharing?.allowPublicVoting
|
|
115
|
+
? t("cards.visibility.items.publicVotingEnabled")
|
|
116
|
+
: t("cards.visibility.items.publicVotingDisabled"),
|
|
117
|
+
portalConfig?.seo?.noIndex
|
|
118
|
+
? t("cards.visibility.items.noIndex")
|
|
119
|
+
: t("cards.visibility.items.index"),
|
|
120
|
+
]}
|
|
121
|
+
href={`/settings/portal-access`}
|
|
122
|
+
actionLabel={t("cards.action")}
|
|
123
|
+
/>
|
|
124
|
+
</div>
|
|
125
|
+
|
|
126
|
+
<details className="group">
|
|
127
|
+
<summary className="cursor-pointer list-none text-sm font-semibold text-slate-700">
|
|
128
|
+
<span className="inline-flex items-center gap-2">
|
|
129
|
+
<span className="rounded-full bg-slate-200 px-2 py-0.5 text-xs font-medium text-slate-600">
|
|
130
|
+
{t("modules.badge")}
|
|
131
|
+
</span>
|
|
132
|
+
{t("modules.summary")}
|
|
133
|
+
</span>
|
|
134
|
+
</summary>
|
|
135
|
+
<div className="mt-4">
|
|
136
|
+
<PortalModulesPanel organizationId={organizationId} initialModules={modules} />
|
|
137
|
+
</div>
|
|
138
|
+
</details>
|
|
139
|
+
</div>
|
|
140
|
+
);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function PortalGroupCard({
|
|
144
|
+
title,
|
|
145
|
+
description,
|
|
146
|
+
items,
|
|
147
|
+
href,
|
|
148
|
+
actionLabel,
|
|
149
|
+
}: {
|
|
150
|
+
title: string;
|
|
151
|
+
description: string;
|
|
152
|
+
items: string[];
|
|
153
|
+
href: string;
|
|
154
|
+
actionLabel: string;
|
|
155
|
+
}) {
|
|
156
|
+
return (
|
|
157
|
+
<Card className="border-slate-200/80 bg-white/80 shadow-sm">
|
|
158
|
+
<CardHeader>
|
|
159
|
+
<CardTitle className="text-lg">{title}</CardTitle>
|
|
160
|
+
<CardDescription>{description}</CardDescription>
|
|
161
|
+
</CardHeader>
|
|
162
|
+
<CardContent className="space-y-3">
|
|
163
|
+
<ul className="space-y-1 text-sm text-slate-600">
|
|
164
|
+
{items.map((item) => (
|
|
165
|
+
<li key={item}>• {item}</li>
|
|
166
|
+
))}
|
|
167
|
+
</ul>
|
|
168
|
+
<Button asChild variant="outline" size="sm">
|
|
169
|
+
<Link href={href}>{actionLabel}</Link>
|
|
170
|
+
</Button>
|
|
171
|
+
</CardContent>
|
|
172
|
+
</Card>
|
|
173
|
+
);
|
|
174
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
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 } from "next/navigation";
|
|
23
|
+
import { cn } from "@/lib/utils";
|
|
24
|
+
|
|
25
|
+
const navItems = [
|
|
26
|
+
{ label: "概览", slug: "" },
|
|
27
|
+
{ label: "体验", slug: "/experience" },
|
|
28
|
+
{ label: "传播", slug: "/growth" },
|
|
29
|
+
{ label: "可见性", slug: "/access" },
|
|
30
|
+
];
|
|
31
|
+
|
|
32
|
+
interface PortalSettingsNavProps {
|
|
33
|
+
baseHref: string;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function PortalSettingsNav({ baseHref }: PortalSettingsNavProps) {
|
|
37
|
+
const pathname = usePathname();
|
|
38
|
+
|
|
39
|
+
return (
|
|
40
|
+
<nav className="flex flex-wrap gap-2 rounded-xl border bg-white/70 p-2 shadow-sm">
|
|
41
|
+
{navItems.map((item) => {
|
|
42
|
+
const href = `${baseHref}${item.slug}`;
|
|
43
|
+
const isActive = pathname === href;
|
|
44
|
+
|
|
45
|
+
return (
|
|
46
|
+
<Link
|
|
47
|
+
key={href}
|
|
48
|
+
href={href}
|
|
49
|
+
className={cn(
|
|
50
|
+
"rounded-lg px-3 py-2 text-sm font-medium transition-colors",
|
|
51
|
+
isActive
|
|
52
|
+
? "bg-slate-900 text-white"
|
|
53
|
+
: "text-slate-700 hover:bg-slate-100",
|
|
54
|
+
)}
|
|
55
|
+
>
|
|
56
|
+
{item.label}
|
|
57
|
+
</Link>
|
|
58
|
+
);
|
|
59
|
+
})}
|
|
60
|
+
</nav>
|
|
61
|
+
);
|
|
62
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
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 { Skeleton } from "@/components/ui/skeleton";
|
|
22
|
+
import { Card, CardContent } from "@/components/ui/card";
|
|
23
|
+
|
|
24
|
+
interface PortalSettingsShellProps {
|
|
25
|
+
title: string;
|
|
26
|
+
description: string;
|
|
27
|
+
children: React.ReactNode;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function PortalSettingsShell({
|
|
31
|
+
title,
|
|
32
|
+
description,
|
|
33
|
+
children,
|
|
34
|
+
}: PortalSettingsShellProps) {
|
|
35
|
+
return (
|
|
36
|
+
<div className="space-y-6">
|
|
37
|
+
<div>
|
|
38
|
+
<h1 className="text-2xl font-bold tracking-tight">{title}</h1>
|
|
39
|
+
<p className="text-muted-foreground">{description}</p>
|
|
40
|
+
</div>
|
|
41
|
+
|
|
42
|
+
{/* Settings Form Content */}
|
|
43
|
+
<Card>
|
|
44
|
+
<CardContent className="pt-6">{children}</CardContent>
|
|
45
|
+
</Card>
|
|
46
|
+
</div>
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function PortalSettingsShellSkeleton() {
|
|
51
|
+
return (
|
|
52
|
+
<div className="space-y-6">
|
|
53
|
+
<div className="flex items-center justify-between">
|
|
54
|
+
<div className="space-y-2">
|
|
55
|
+
<Skeleton className="h-8 w-48" />
|
|
56
|
+
<Skeleton className="h-4 w-64" />
|
|
57
|
+
</div>
|
|
58
|
+
<Skeleton className="h-10 w-[200px]" />
|
|
59
|
+
</div>
|
|
60
|
+
<Card>
|
|
61
|
+
<CardContent className="pt-6">
|
|
62
|
+
<div className="space-y-4">
|
|
63
|
+
<Skeleton className="h-10 w-full" />
|
|
64
|
+
<Skeleton className="h-10 w-full" />
|
|
65
|
+
<Skeleton className="h-10 w-full" />
|
|
66
|
+
</div>
|
|
67
|
+
</CardContent>
|
|
68
|
+
</Card>
|
|
69
|
+
</div>
|
|
70
|
+
);
|
|
71
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
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 { Suspense } from "react";
|
|
19
|
+
import { PortalNav } from "./portal-nav";
|
|
20
|
+
import { EmbeddedFeedbackForm } from "@/components/feedback/embedded-feedback-form";
|
|
21
|
+
|
|
22
|
+
type Section = {
|
|
23
|
+
label: string;
|
|
24
|
+
href: string;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
type PortalShellProps = {
|
|
28
|
+
title: string;
|
|
29
|
+
sections: readonly Section[];
|
|
30
|
+
organizationId?: string;
|
|
31
|
+
children?: React.ReactNode;
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
export function PortalShell({ title, sections, organizationId, children }: PortalShellProps) {
|
|
35
|
+
return (
|
|
36
|
+
<div className="min-h-screen bg-background">
|
|
37
|
+
<header className="sticky top-0 z-40 border-b bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60">
|
|
38
|
+
<div className="container mx-auto flex h-16 items-center justify-between px-4">
|
|
39
|
+
<h1 className="text-xl font-semibold">{title}</h1>
|
|
40
|
+
<PortalNav sections={sections} />
|
|
41
|
+
</div>
|
|
42
|
+
</header>
|
|
43
|
+
<main className="container mx-auto px-4 py-8">
|
|
44
|
+
<div className="mx-auto max-w-2xl space-y-8">
|
|
45
|
+
{children ?? (
|
|
46
|
+
<>
|
|
47
|
+
<div className="text-center">
|
|
48
|
+
<h2 className="text-2xl font-bold tracking-tight">分享你的反馈</h2>
|
|
49
|
+
<p className="mt-2 text-muted-foreground">
|
|
50
|
+
帮助我们了解你的想法,让产品变得更好
|
|
51
|
+
</p>
|
|
52
|
+
</div>
|
|
53
|
+
<Suspense fallback={<div className="animate-pulse h-40 bg-muted rounded-lg" />}>
|
|
54
|
+
<EmbeddedFeedbackForm organizationId={organizationId} />
|
|
55
|
+
</Suspense>
|
|
56
|
+
</>
|
|
57
|
+
)}
|
|
58
|
+
</div>
|
|
59
|
+
</main>
|
|
60
|
+
</div>
|
|
61
|
+
);
|
|
62
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
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 } from "next/navigation";
|
|
23
|
+
import { cn } from "@/lib/utils";
|
|
24
|
+
|
|
25
|
+
export interface PortalSection {
|
|
26
|
+
label: string;
|
|
27
|
+
href: string;
|
|
28
|
+
icon?: React.ReactNode;
|
|
29
|
+
exact?: boolean;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
interface PortalTabNavProps {
|
|
33
|
+
sections: readonly PortalSection[];
|
|
34
|
+
className?: string;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function PortalTabNav({ sections, className }: PortalTabNavProps) {
|
|
38
|
+
const pathname = usePathname();
|
|
39
|
+
|
|
40
|
+
return (
|
|
41
|
+
<div className={cn("border-b bg-background", className)}>
|
|
42
|
+
<div className="container mx-auto px-4">
|
|
43
|
+
<nav className="flex items-center gap-2 py-2" role="tablist">
|
|
44
|
+
{sections.map((section) => {
|
|
45
|
+
// Check if current path matches this section
|
|
46
|
+
const isActive = section.exact
|
|
47
|
+
? pathname === section.href
|
|
48
|
+
: pathname === section.href ||
|
|
49
|
+
(section.href !== "/" && pathname.startsWith(section.href + "/"));
|
|
50
|
+
|
|
51
|
+
return (
|
|
52
|
+
<Link
|
|
53
|
+
key={section.href}
|
|
54
|
+
href={section.href}
|
|
55
|
+
role="tab"
|
|
56
|
+
aria-selected={isActive}
|
|
57
|
+
className={cn(
|
|
58
|
+
"relative inline-flex h-10 items-center px-4 text-sm font-medium transition-colors",
|
|
59
|
+
"rounded-full border border-transparent hover:bg-muted/60 hover:border-border/40",
|
|
60
|
+
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
|
|
61
|
+
isActive
|
|
62
|
+
? "bg-muted/70 text-foreground border-border/60 font-semibold shadow-[inset_0_-1px_0_0_hsl(var(--border)),0_1px_2px_hsl(var(--foreground)/0.08)]"
|
|
63
|
+
: "text-muted-foreground/70 hover:text-foreground"
|
|
64
|
+
)}
|
|
65
|
+
>
|
|
66
|
+
<span className="inline-flex items-center gap-2 leading-none [&_svg]:size-4 [&_svg]:align-middle">
|
|
67
|
+
{section.icon}
|
|
68
|
+
{section.label}
|
|
69
|
+
</span>
|
|
70
|
+
</Link>
|
|
71
|
+
);
|
|
72
|
+
})}
|
|
73
|
+
</nav>
|
|
74
|
+
</div>
|
|
75
|
+
</div>
|
|
76
|
+
);
|
|
77
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright (c) 2026 Echo Team
|
|
3
|
+
*
|
|
4
|
+
* This program is free software: you can redistribute it and/or modify
|
|
5
|
+
* it under the terms of the GNU Affero General Public License as published by
|
|
6
|
+
* the Free Software Foundation, either version 3 of the License, or
|
|
7
|
+
* (at your option) any later version.
|
|
8
|
+
*
|
|
9
|
+
* This program is distributed in the hope that it will be useful,
|
|
10
|
+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
11
|
+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
12
|
+
* GNU Affero General Public License for more details.
|
|
13
|
+
*
|
|
14
|
+
* You should have received a copy of the GNU Affero General Public License
|
|
15
|
+
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
export function ProjectSwitcher() {
|
|
19
|
+
return null;
|
|
20
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
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 { RoadmapColumn, type RoadmapItem } from "./roadmap-column";
|
|
22
|
+
import { Button } from "@/components/ui/button";
|
|
23
|
+
import { Edit } from "lucide-react";
|
|
24
|
+
import { cn } from "@/lib/utils";
|
|
25
|
+
|
|
26
|
+
export type RoadmapStatus = "backlog" | "next-up" | "in-progress" | "done";
|
|
27
|
+
|
|
28
|
+
interface RoadmapBoardProps {
|
|
29
|
+
items: RoadmapItem[];
|
|
30
|
+
isAdmin?: boolean;
|
|
31
|
+
className?: string;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const columns: { status: RoadmapStatus; label: string; color: string }[] = [
|
|
35
|
+
{ status: "backlog", label: "Backlog", color: "bg-gray-100 dark:bg-gray-800" },
|
|
36
|
+
{ status: "next-up", label: "Next Up", color: "bg-blue-50 dark:bg-blue-950" },
|
|
37
|
+
{ status: "in-progress", label: "In Progress", color: "bg-amber-50 dark:bg-amber-950" },
|
|
38
|
+
{ status: "done", label: "Done", color: "bg-green-50 dark:bg-green-950" },
|
|
39
|
+
];
|
|
40
|
+
|
|
41
|
+
export function RoadmapBoard({
|
|
42
|
+
items,
|
|
43
|
+
isAdmin = false,
|
|
44
|
+
className,
|
|
45
|
+
}: RoadmapBoardProps) {
|
|
46
|
+
// Group items by status
|
|
47
|
+
const groupedItems = columns.reduce(
|
|
48
|
+
(acc, column) => {
|
|
49
|
+
acc[column.status] = items.filter((item) => item.status === column.status);
|
|
50
|
+
return acc;
|
|
51
|
+
},
|
|
52
|
+
{} as Record<RoadmapStatus, RoadmapItem[]>
|
|
53
|
+
);
|
|
54
|
+
|
|
55
|
+
return (
|
|
56
|
+
<div className={cn("space-y-4", className)}>
|
|
57
|
+
{/* Kanban Board */}
|
|
58
|
+
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
|
|
59
|
+
{columns.map((column) => (
|
|
60
|
+
<RoadmapColumn
|
|
61
|
+
key={column.status}
|
|
62
|
+
status={column.status}
|
|
63
|
+
label={column.label}
|
|
64
|
+
items={groupedItems[column.status]}
|
|
65
|
+
headerColor={column.color}
|
|
66
|
+
/>
|
|
67
|
+
))}
|
|
68
|
+
</div>
|
|
69
|
+
|
|
70
|
+
{/* Admin Edit Button */}
|
|
71
|
+
{isAdmin && (
|
|
72
|
+
<Button
|
|
73
|
+
variant="secondary"
|
|
74
|
+
className="fixed bottom-6 right-6 shadow-lg gap-2 z-40"
|
|
75
|
+
>
|
|
76
|
+
<Edit className="h-4 w-4" />
|
|
77
|
+
Edit Roadmap
|
|
78
|
+
</Button>
|
|
79
|
+
)}
|
|
80
|
+
</div>
|
|
81
|
+
);
|
|
82
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
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 { ChevronUp } from "lucide-react";
|
|
23
|
+
import { Badge } from "@/components/ui/badge";
|
|
24
|
+
import { Button } from "@/components/ui/button";
|
|
25
|
+
import { cn } from "@/lib/utils";
|
|
26
|
+
import type { RoadmapItem } from "./roadmap-column";
|
|
27
|
+
|
|
28
|
+
interface RoadmapCardProps {
|
|
29
|
+
item: RoadmapItem;
|
|
30
|
+
className?: string;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function RoadmapCard({ item, className }: RoadmapCardProps) {
|
|
34
|
+
const handleVote = async (e: React.MouseEvent) => {
|
|
35
|
+
e.preventDefault();
|
|
36
|
+
e.stopPropagation();
|
|
37
|
+
// TODO: Implement voting API call
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
return (
|
|
41
|
+
<Link
|
|
42
|
+
href={`/feedback/${item.id}`}
|
|
43
|
+
className={cn(
|
|
44
|
+
"group block rounded-lg border bg-card p-3 shadow-sm transition-all",
|
|
45
|
+
"hover:shadow-md hover:border-primary/30 hover:-translate-y-0.5",
|
|
46
|
+
className
|
|
47
|
+
)}
|
|
48
|
+
>
|
|
49
|
+
<div className="space-y-2">
|
|
50
|
+
{/* Title */}
|
|
51
|
+
<h4 className="font-medium text-sm line-clamp-2">{item.title}</h4>
|
|
52
|
+
|
|
53
|
+
{/* Footer */}
|
|
54
|
+
<div className="flex items-center justify-between">
|
|
55
|
+
{/* Category Tag */}
|
|
56
|
+
{item.category && (
|
|
57
|
+
<Badge variant="secondary" className="text-xs">
|
|
58
|
+
{item.category}
|
|
59
|
+
</Badge>
|
|
60
|
+
)}
|
|
61
|
+
|
|
62
|
+
{/* Vote Button */}
|
|
63
|
+
<Button
|
|
64
|
+
variant="ghost"
|
|
65
|
+
size="sm"
|
|
66
|
+
className="h-auto gap-1 px-2 py-1 text-xs hover:bg-primary/10 hover:text-primary"
|
|
67
|
+
onClick={handleVote}
|
|
68
|
+
>
|
|
69
|
+
<ChevronUp className="h-3 w-3" />
|
|
70
|
+
<span>{item.voteCount}</span>
|
|
71
|
+
</Button>
|
|
72
|
+
</div>
|
|
73
|
+
</div>
|
|
74
|
+
</Link>
|
|
75
|
+
);
|
|
76
|
+
}
|