@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,156 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
/*
|
|
5
|
+
* Copyright (c) 2026 Echo Team
|
|
6
|
+
*
|
|
7
|
+
* This program is free software: you can redistribute it and/or modify
|
|
8
|
+
* it under the terms of the GNU Affero General Public License as published by
|
|
9
|
+
* the Free Software Foundation, either version 3 of the License, or
|
|
10
|
+
* (at your option) any later version.
|
|
11
|
+
*
|
|
12
|
+
* This program is distributed in the hope that it will be useful,
|
|
13
|
+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
14
|
+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
15
|
+
* GNU Affero General Public License for more details.
|
|
16
|
+
*
|
|
17
|
+
* You should have received a copy of the GNU Affero General Public License
|
|
18
|
+
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import { useState } from "react";
|
|
22
|
+
import { useForm, useWatch } from "react-hook-form";
|
|
23
|
+
import { Button } from "@/components/ui/button";
|
|
24
|
+
import { Input } from "@/components/ui/input";
|
|
25
|
+
import { Label } from "@/components/ui/label";
|
|
26
|
+
import { Textarea } from "@/components/ui/textarea";
|
|
27
|
+
import { Switch } from "@/components/ui/switch";
|
|
28
|
+
import { Loader2 } from "lucide-react";
|
|
29
|
+
import { updatePortalSettings } from "@/lib/services/portal-settings";
|
|
30
|
+
import type { PortalSeoConfig } from "@/lib/db/schema";
|
|
31
|
+
|
|
32
|
+
interface SeoFormProps {
|
|
33
|
+
organizationId: string;
|
|
34
|
+
initialData?: PortalSeoConfig;
|
|
35
|
+
showNoIndex?: boolean;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function SeoForm({ organizationId, initialData, showNoIndex = true }: SeoFormProps) {
|
|
39
|
+
const [saving, setSaving] = useState(false);
|
|
40
|
+
const [message, setMessage] = useState<{ type: "success" | "error"; text: string } | null>(null);
|
|
41
|
+
|
|
42
|
+
const form = useForm<PortalSeoConfig>({
|
|
43
|
+
defaultValues: {
|
|
44
|
+
metaTitle: initialData?.metaTitle ?? "",
|
|
45
|
+
metaDescription: initialData?.metaDescription ?? "",
|
|
46
|
+
ogImage: initialData?.ogImage ?? "",
|
|
47
|
+
favicon: initialData?.favicon ?? "",
|
|
48
|
+
noIndex: initialData?.noIndex ?? false,
|
|
49
|
+
},
|
|
50
|
+
});
|
|
51
|
+
const noIndex = useWatch({ control: form.control, name: "noIndex" }) ?? false;
|
|
52
|
+
|
|
53
|
+
const onSubmit = async (data: PortalSeoConfig) => {
|
|
54
|
+
setSaving(true);
|
|
55
|
+
setMessage(null);
|
|
56
|
+
|
|
57
|
+
const result = await updatePortalSettings(organizationId, "seo", data);
|
|
58
|
+
|
|
59
|
+
if (result.success) {
|
|
60
|
+
setMessage({ type: "success", text: "SEO 设置已保存" });
|
|
61
|
+
} else {
|
|
62
|
+
setMessage({ type: "error", text: result.error || "保存失败" });
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
setSaving(false);
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
return (
|
|
69
|
+
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-6">
|
|
70
|
+
{/* Meta Title */}
|
|
71
|
+
<div className="space-y-2">
|
|
72
|
+
<Label htmlFor="metaTitle">页面标题 (Title Tag)</Label>
|
|
73
|
+
<Input
|
|
74
|
+
id="metaTitle"
|
|
75
|
+
placeholder="产品反馈 | 您的公司名"
|
|
76
|
+
{...form.register("metaTitle")}
|
|
77
|
+
/>
|
|
78
|
+
<p className="text-sm text-muted-foreground">显示在浏览器标签页和搜索结果中</p>
|
|
79
|
+
</div>
|
|
80
|
+
|
|
81
|
+
{/* Meta Description */}
|
|
82
|
+
<div className="space-y-2">
|
|
83
|
+
<Label htmlFor="metaDescription">页面描述 (Meta Description)</Label>
|
|
84
|
+
<Textarea
|
|
85
|
+
id="metaDescription"
|
|
86
|
+
placeholder="提交产品反馈、查看路线图和变更日志。"
|
|
87
|
+
rows={3}
|
|
88
|
+
{...form.register("metaDescription")}
|
|
89
|
+
/>
|
|
90
|
+
<p className="text-sm text-muted-foreground">
|
|
91
|
+
搜索引擎展示的页面描述,建议 150-160 字符
|
|
92
|
+
</p>
|
|
93
|
+
</div>
|
|
94
|
+
|
|
95
|
+
{/* OG Image */}
|
|
96
|
+
<div className="space-y-2">
|
|
97
|
+
<Label htmlFor="ogImage">社交分享图片 (OG Image)</Label>
|
|
98
|
+
<Input
|
|
99
|
+
id="ogImage"
|
|
100
|
+
type="url"
|
|
101
|
+
placeholder="https://example.com/og-image.png"
|
|
102
|
+
{...form.register("ogImage")}
|
|
103
|
+
/>
|
|
104
|
+
<p className="text-sm text-muted-foreground">
|
|
105
|
+
分享到社交媒体时显示的图片,建议尺寸 1200x630
|
|
106
|
+
</p>
|
|
107
|
+
</div>
|
|
108
|
+
|
|
109
|
+
{/* Favicon */}
|
|
110
|
+
<div className="space-y-2">
|
|
111
|
+
<Label htmlFor="favicon">网站图标 (Favicon)</Label>
|
|
112
|
+
<Input
|
|
113
|
+
id="favicon"
|
|
114
|
+
type="url"
|
|
115
|
+
placeholder="https://example.com/favicon.ico"
|
|
116
|
+
{...form.register("favicon")}
|
|
117
|
+
/>
|
|
118
|
+
<p className="text-sm text-muted-foreground">显示在浏览器标签页的小图标</p>
|
|
119
|
+
</div>
|
|
120
|
+
|
|
121
|
+
{/* No Index */}
|
|
122
|
+
{showNoIndex && (
|
|
123
|
+
<div className="flex items-center justify-between rounded-lg border p-4">
|
|
124
|
+
<div className="space-y-0.5">
|
|
125
|
+
<Label htmlFor="noIndex">阻止搜索引擎索引</Label>
|
|
126
|
+
<p className="text-sm text-muted-foreground">
|
|
127
|
+
启用后搜索引擎将不会收录此页面
|
|
128
|
+
</p>
|
|
129
|
+
</div>
|
|
130
|
+
<Switch
|
|
131
|
+
id="noIndex"
|
|
132
|
+
checked={noIndex}
|
|
133
|
+
onCheckedChange={(checked) => form.setValue("noIndex", checked)}
|
|
134
|
+
/>
|
|
135
|
+
</div>
|
|
136
|
+
)}
|
|
137
|
+
|
|
138
|
+
{/* Submit */}
|
|
139
|
+
<div className="flex items-center gap-4">
|
|
140
|
+
<Button type="submit" disabled={saving}>
|
|
141
|
+
{saving && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
|
|
142
|
+
保存更改
|
|
143
|
+
</Button>
|
|
144
|
+
{message && (
|
|
145
|
+
<p
|
|
146
|
+
className={`text-sm ${
|
|
147
|
+
message.type === "success" ? "text-green-600" : "text-destructive"
|
|
148
|
+
}`}
|
|
149
|
+
>
|
|
150
|
+
{message.text}
|
|
151
|
+
</p>
|
|
152
|
+
)}
|
|
153
|
+
</div>
|
|
154
|
+
</form>
|
|
155
|
+
);
|
|
156
|
+
}
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
/*
|
|
5
|
+
* Copyright (c) 2026 Echo Team
|
|
6
|
+
*
|
|
7
|
+
* This program is free software: you can redistribute it and/or modify
|
|
8
|
+
* it under the terms of the GNU Affero General Public License as published by
|
|
9
|
+
* the Free Software Foundation, either version 3 of the License, or
|
|
10
|
+
* (at your option) any later version.
|
|
11
|
+
*
|
|
12
|
+
* This program is distributed in the hope that it will be useful,
|
|
13
|
+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
14
|
+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
15
|
+
* GNU Affero General Public License for more details.
|
|
16
|
+
*
|
|
17
|
+
* You should have received a copy of the GNU Affero General Public License
|
|
18
|
+
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import { useState } from "react";
|
|
22
|
+
import { useForm, useWatch } from "react-hook-form";
|
|
23
|
+
import { Button } from "@/components/ui/button";
|
|
24
|
+
import { Label } from "@/components/ui/label";
|
|
25
|
+
import { Switch } from "@/components/ui/switch";
|
|
26
|
+
import { Loader2 } from "lucide-react";
|
|
27
|
+
import { updatePortalSettings } from "@/lib/services/portal-settings";
|
|
28
|
+
import type { PortalSharingConfig } from "@/lib/db/schema";
|
|
29
|
+
|
|
30
|
+
interface SharingFormProps {
|
|
31
|
+
organizationId: string;
|
|
32
|
+
initialData?: PortalSharingConfig;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
type SharingFormData = {
|
|
36
|
+
socialSharing: {
|
|
37
|
+
twitter: boolean;
|
|
38
|
+
linkedin: boolean;
|
|
39
|
+
facebook: boolean;
|
|
40
|
+
};
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
function SwitchField({
|
|
44
|
+
id,
|
|
45
|
+
label,
|
|
46
|
+
description,
|
|
47
|
+
checked,
|
|
48
|
+
onCheckedChange,
|
|
49
|
+
}: {
|
|
50
|
+
id: string;
|
|
51
|
+
label: string;
|
|
52
|
+
description: string;
|
|
53
|
+
checked: boolean;
|
|
54
|
+
onCheckedChange: (checked: boolean) => void;
|
|
55
|
+
}) {
|
|
56
|
+
return (
|
|
57
|
+
<div className="flex items-center justify-between rounded-lg border p-4">
|
|
58
|
+
<div className="space-y-0.5">
|
|
59
|
+
<Label htmlFor={id}>{label}</Label>
|
|
60
|
+
<p className="text-sm text-muted-foreground">{description}</p>
|
|
61
|
+
</div>
|
|
62
|
+
<Switch id={id} checked={checked} onCheckedChange={onCheckedChange} />
|
|
63
|
+
</div>
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function SharingForm({ organizationId, initialData }: SharingFormProps) {
|
|
68
|
+
const [saving, setSaving] = useState(false);
|
|
69
|
+
const [message, setMessage] = useState<{ type: "success" | "error"; text: string } | null>(null);
|
|
70
|
+
|
|
71
|
+
const form = useForm<SharingFormData>({
|
|
72
|
+
defaultValues: {
|
|
73
|
+
socialSharing: {
|
|
74
|
+
twitter: initialData?.socialSharing?.twitter ?? true,
|
|
75
|
+
linkedin: initialData?.socialSharing?.linkedin ?? true,
|
|
76
|
+
facebook: initialData?.socialSharing?.facebook ?? false,
|
|
77
|
+
},
|
|
78
|
+
},
|
|
79
|
+
});
|
|
80
|
+
const socialSharing = useWatch({ control: form.control, name: "socialSharing" }) ?? {
|
|
81
|
+
twitter: true,
|
|
82
|
+
linkedin: true,
|
|
83
|
+
facebook: false,
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
const onSubmit = async (data: SharingFormData) => {
|
|
87
|
+
setSaving(true);
|
|
88
|
+
setMessage(null);
|
|
89
|
+
|
|
90
|
+
const payload: PortalSharingConfig = {
|
|
91
|
+
...initialData,
|
|
92
|
+
socialSharing: data.socialSharing,
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
const result = await updatePortalSettings(organizationId, "sharing", payload);
|
|
96
|
+
|
|
97
|
+
if (result.success) {
|
|
98
|
+
setMessage({ type: "success", text: "分享设置已保存" });
|
|
99
|
+
} else {
|
|
100
|
+
setMessage({ type: "error", text: result.error || "保存失败" });
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
setSaving(false);
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
return (
|
|
107
|
+
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-6">
|
|
108
|
+
{/* Social Sharing Section */}
|
|
109
|
+
<div className="space-y-4">
|
|
110
|
+
<h3 className="text-sm font-medium">社交分享按钮</h3>
|
|
111
|
+
|
|
112
|
+
<SwitchField
|
|
113
|
+
id="socialSharing.twitter"
|
|
114
|
+
label="Twitter / X"
|
|
115
|
+
description="显示分享到 Twitter 的按钮"
|
|
116
|
+
checked={socialSharing.twitter ?? true}
|
|
117
|
+
onCheckedChange={(checked) => form.setValue("socialSharing.twitter", checked)}
|
|
118
|
+
/>
|
|
119
|
+
|
|
120
|
+
<SwitchField
|
|
121
|
+
id="socialSharing.linkedin"
|
|
122
|
+
label="LinkedIn"
|
|
123
|
+
description="显示分享到 LinkedIn 的按钮"
|
|
124
|
+
checked={socialSharing.linkedin ?? true}
|
|
125
|
+
onCheckedChange={(checked) => form.setValue("socialSharing.linkedin", checked)}
|
|
126
|
+
/>
|
|
127
|
+
|
|
128
|
+
<SwitchField
|
|
129
|
+
id="socialSharing.facebook"
|
|
130
|
+
label="Facebook"
|
|
131
|
+
description="显示分享到 Facebook 的按钮"
|
|
132
|
+
checked={socialSharing.facebook ?? false}
|
|
133
|
+
onCheckedChange={(checked) => form.setValue("socialSharing.facebook", checked)}
|
|
134
|
+
/>
|
|
135
|
+
</div>
|
|
136
|
+
|
|
137
|
+
{/* Submit */}
|
|
138
|
+
<div className="flex items-center gap-4">
|
|
139
|
+
<Button type="submit" disabled={saving}>
|
|
140
|
+
{saving && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
|
|
141
|
+
保存更改
|
|
142
|
+
</Button>
|
|
143
|
+
{message && (
|
|
144
|
+
<p
|
|
145
|
+
className={`text-sm ${
|
|
146
|
+
message.type === "success" ? "text-green-600" : "text-destructive"
|
|
147
|
+
}`}
|
|
148
|
+
>
|
|
149
|
+
{message.text}
|
|
150
|
+
</p>
|
|
151
|
+
)}
|
|
152
|
+
</div>
|
|
153
|
+
</form>
|
|
154
|
+
);
|
|
155
|
+
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
/*
|
|
5
|
+
* Copyright (c) 2026 Echo Team
|
|
6
|
+
*
|
|
7
|
+
* This program is free software: you can redistribute it and/or modify
|
|
8
|
+
* it under the terms of the GNU Affero General Public License as published by
|
|
9
|
+
* the Free Software Foundation, either version 3 of the License, or
|
|
10
|
+
* (at your option) any later version.
|
|
11
|
+
*
|
|
12
|
+
* This program is distributed in the hope that it will be useful,
|
|
13
|
+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
14
|
+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
15
|
+
* GNU Affero General Public License for more details.
|
|
16
|
+
*
|
|
17
|
+
* You should have received a copy of the GNU Affero General Public License
|
|
18
|
+
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import { useState } from "react";
|
|
22
|
+
import { useTranslations } from "next-intl";
|
|
23
|
+
import { useForm } from "react-hook-form";
|
|
24
|
+
import { Button } from "@/components/ui/button";
|
|
25
|
+
import { Input } from "@/components/ui/input";
|
|
26
|
+
import { Label } from "@/components/ui/label";
|
|
27
|
+
import { Loader2 } from "lucide-react";
|
|
28
|
+
import { updatePortalSettings } from "@/lib/services/portal-settings";
|
|
29
|
+
import type { PortalThemeConfig } from "@/lib/db/schema";
|
|
30
|
+
|
|
31
|
+
interface ThemeFormProps {
|
|
32
|
+
organizationId: string;
|
|
33
|
+
initialData?: PortalThemeConfig;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
type ThemeFormData = Pick<PortalThemeConfig, "primaryColor">;
|
|
37
|
+
|
|
38
|
+
export function ThemeForm({ organizationId, initialData }: ThemeFormProps) {
|
|
39
|
+
const t = useTranslations("settings.portal.branding.themeForm");
|
|
40
|
+
const [saving, setSaving] = useState(false);
|
|
41
|
+
const [message, setMessage] = useState<{ type: "success" | "error"; text: string } | null>(null);
|
|
42
|
+
|
|
43
|
+
const form = useForm<ThemeFormData>({
|
|
44
|
+
defaultValues: {
|
|
45
|
+
primaryColor: initialData?.primaryColor ?? "#6366f1",
|
|
46
|
+
},
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
const onSubmit = async (data: ThemeFormData) => {
|
|
50
|
+
setSaving(true);
|
|
51
|
+
setMessage(null);
|
|
52
|
+
|
|
53
|
+
const result = await updatePortalSettings(organizationId, "theme", data);
|
|
54
|
+
|
|
55
|
+
if (result.success) {
|
|
56
|
+
setMessage({ type: "success", text: t("status.saved") });
|
|
57
|
+
} else {
|
|
58
|
+
setMessage({ type: "error", text: result.error || t("status.saveFailed") });
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
setSaving(false);
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
return (
|
|
65
|
+
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-6">
|
|
66
|
+
{/* Primary Color */}
|
|
67
|
+
<div className="space-y-2">
|
|
68
|
+
<Label htmlFor="primaryColor">{t("labels.primaryColor")}</Label>
|
|
69
|
+
<div className="flex items-center gap-3">
|
|
70
|
+
<Input
|
|
71
|
+
id="primaryColor"
|
|
72
|
+
type="color"
|
|
73
|
+
className="w-16 h-10 p-1 cursor-pointer"
|
|
74
|
+
{...form.register("primaryColor")}
|
|
75
|
+
/>
|
|
76
|
+
<Input
|
|
77
|
+
type="text"
|
|
78
|
+
className="flex-1"
|
|
79
|
+
placeholder="#6366f1"
|
|
80
|
+
{...form.register("primaryColor")}
|
|
81
|
+
/>
|
|
82
|
+
</div>
|
|
83
|
+
<p className="text-sm text-muted-foreground">{t("help.primaryColor")}</p>
|
|
84
|
+
</div>
|
|
85
|
+
|
|
86
|
+
{/* Submit */}
|
|
87
|
+
<div className="flex items-center gap-4">
|
|
88
|
+
<Button type="submit" disabled={saving}>
|
|
89
|
+
{saving && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
|
|
90
|
+
{t("buttons.save")}
|
|
91
|
+
</Button>
|
|
92
|
+
{message && (
|
|
93
|
+
<p
|
|
94
|
+
className={`text-sm ${
|
|
95
|
+
message.type === "success" ? "text-green-600" : "text-destructive"
|
|
96
|
+
}`}
|
|
97
|
+
>
|
|
98
|
+
{message.text}
|
|
99
|
+
</p>
|
|
100
|
+
)}
|
|
101
|
+
</div>
|
|
102
|
+
</form>
|
|
103
|
+
);
|
|
104
|
+
}
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
/*
|
|
5
|
+
* Copyright (c) 2026 Echo Team
|
|
6
|
+
*
|
|
7
|
+
* This program is free software: you can redistribute it and/or modify
|
|
8
|
+
* it under the terms of the GNU Affero General Public License as published by
|
|
9
|
+
* the Free Software Foundation, either version 3 of the License, or
|
|
10
|
+
* (at your option) any later version.
|
|
11
|
+
*
|
|
12
|
+
* This program is distributed in the hope that it will be useful,
|
|
13
|
+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
14
|
+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
15
|
+
* GNU Affero General Public License for more details.
|
|
16
|
+
*
|
|
17
|
+
* You should have received a copy of the GNU Affero General Public License
|
|
18
|
+
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import { useState, useEffect } from "react";
|
|
22
|
+
import { useTranslations } from "next-intl";
|
|
23
|
+
import { Button } from "@/components/ui/button";
|
|
24
|
+
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
|
25
|
+
import { Input } from "@/components/ui/input";
|
|
26
|
+
import { Label } from "@/components/ui/label";
|
|
27
|
+
import { Key, Copy, Trash2, Plus } from "lucide-react";
|
|
28
|
+
|
|
29
|
+
interface ApiKey {
|
|
30
|
+
keyId: string;
|
|
31
|
+
name: string;
|
|
32
|
+
prefix: string;
|
|
33
|
+
displayKey: string;
|
|
34
|
+
createdAt: string;
|
|
35
|
+
lastUsed: string | null;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function ApiKeysList() {
|
|
39
|
+
const [keys, setKeys] = useState<ApiKey[]>([]);
|
|
40
|
+
const [isLoading, setIsLoading] = useState(true);
|
|
41
|
+
const [newKeyName, setNewKeyName] = useState("");
|
|
42
|
+
const [isCreating, setIsCreating] = useState(false);
|
|
43
|
+
const t = useTranslations("settings.apiKeys");
|
|
44
|
+
const tCommon = useTranslations("common");
|
|
45
|
+
|
|
46
|
+
useEffect(() => {
|
|
47
|
+
fetchKeys();
|
|
48
|
+
}, []);
|
|
49
|
+
|
|
50
|
+
const fetchKeys = async () => {
|
|
51
|
+
try {
|
|
52
|
+
const res = await fetch("/api/api-keys");
|
|
53
|
+
if (res.ok) {
|
|
54
|
+
const data = await res.json();
|
|
55
|
+
setKeys(data.data || []);
|
|
56
|
+
}
|
|
57
|
+
} catch (error) {
|
|
58
|
+
console.error("Failed to fetch API keys:", error);
|
|
59
|
+
} finally {
|
|
60
|
+
setIsLoading(false);
|
|
61
|
+
}
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
const handleCreate = async (e: React.FormEvent) => {
|
|
65
|
+
e.preventDefault();
|
|
66
|
+
if (!newKeyName.trim()) return;
|
|
67
|
+
|
|
68
|
+
setIsCreating(true);
|
|
69
|
+
try {
|
|
70
|
+
const res = await fetch("/api/api-keys", {
|
|
71
|
+
method: "POST",
|
|
72
|
+
headers: { "Content-Type": "application/json" },
|
|
73
|
+
body: JSON.stringify({ name: newKeyName }),
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
if (res.ok) {
|
|
77
|
+
const data = await res.json();
|
|
78
|
+
alert(t("keyCreatedAlert", { key: data.data.key }));
|
|
79
|
+
setNewKeyName("");
|
|
80
|
+
fetchKeys();
|
|
81
|
+
}
|
|
82
|
+
} catch (error) {
|
|
83
|
+
console.error("Failed to create API key:", error);
|
|
84
|
+
} finally {
|
|
85
|
+
setIsCreating(false);
|
|
86
|
+
}
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
const handleDelete = async (keyId: string) => {
|
|
90
|
+
if (!confirm(t("deleteConfirm"))) return;
|
|
91
|
+
|
|
92
|
+
try {
|
|
93
|
+
const res = await fetch(`/api/api-keys/${keyId}`, { method: "DELETE" });
|
|
94
|
+
if (res.ok) {
|
|
95
|
+
fetchKeys();
|
|
96
|
+
}
|
|
97
|
+
} catch (error) {
|
|
98
|
+
console.error("Failed to delete API key:", error);
|
|
99
|
+
}
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
if (isLoading) {
|
|
103
|
+
return <div className="text-muted-foreground">{tCommon("loading")}</div>;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return (
|
|
107
|
+
<div className="space-y-6">
|
|
108
|
+
<Card>
|
|
109
|
+
<CardHeader>
|
|
110
|
+
<CardTitle>{t("createTitle")}</CardTitle>
|
|
111
|
+
<CardDescription>{t("createDescription")}</CardDescription>
|
|
112
|
+
</CardHeader>
|
|
113
|
+
<CardContent>
|
|
114
|
+
<form onSubmit={handleCreate} className="flex gap-4">
|
|
115
|
+
<div className="flex-1">
|
|
116
|
+
<Label htmlFor="keyName" className="sr-only">{t("keyNameLabel")}</Label>
|
|
117
|
+
<Input
|
|
118
|
+
id="keyName"
|
|
119
|
+
value={newKeyName}
|
|
120
|
+
onChange={(e) => setNewKeyName(e.target.value)}
|
|
121
|
+
placeholder={t("keyNamePlaceholder")}
|
|
122
|
+
/>
|
|
123
|
+
</div>
|
|
124
|
+
<Button type="submit" disabled={isCreating || !newKeyName.trim()}>
|
|
125
|
+
<Plus className="mr-2 h-4 w-4" />
|
|
126
|
+
{t("createButton")}
|
|
127
|
+
</Button>
|
|
128
|
+
</form>
|
|
129
|
+
</CardContent>
|
|
130
|
+
</Card>
|
|
131
|
+
|
|
132
|
+
<Card>
|
|
133
|
+
<CardHeader>
|
|
134
|
+
<CardTitle>{t("existingTitle")}</CardTitle>
|
|
135
|
+
<CardDescription>{t("existingDescription")}</CardDescription>
|
|
136
|
+
</CardHeader>
|
|
137
|
+
<CardContent>
|
|
138
|
+
{keys.length === 0 ? (
|
|
139
|
+
<p className="text-muted-foreground py-4">{t("noKeys")}</p>
|
|
140
|
+
) : (
|
|
141
|
+
<div className="space-y-4">
|
|
142
|
+
{keys.map((key) => (
|
|
143
|
+
<div key={key.keyId} className="flex items-center justify-between p-4 border rounded-lg">
|
|
144
|
+
<div className="flex items-center gap-3">
|
|
145
|
+
<Key className="h-5 w-5 text-muted-foreground" />
|
|
146
|
+
<div>
|
|
147
|
+
<p className="font-medium">{key.name}</p>
|
|
148
|
+
<p className="text-sm text-muted-foreground font-mono">{key.displayKey}</p>
|
|
149
|
+
</div>
|
|
150
|
+
</div>
|
|
151
|
+
<div className="flex items-center gap-2">
|
|
152
|
+
<Button variant="ghost" size="icon" onClick={() => navigator.clipboard.writeText(key.prefix)}>
|
|
153
|
+
<Copy className="h-4 w-4" />
|
|
154
|
+
</Button>
|
|
155
|
+
<Button variant="ghost" size="icon" onClick={() => handleDelete(key.keyId)}>
|
|
156
|
+
<Trash2 className="h-4 w-4 text-destructive" />
|
|
157
|
+
</Button>
|
|
158
|
+
</div>
|
|
159
|
+
</div>
|
|
160
|
+
))}
|
|
161
|
+
</div>
|
|
162
|
+
)}
|
|
163
|
+
</CardContent>
|
|
164
|
+
</Card>
|
|
165
|
+
</div>
|
|
166
|
+
);
|
|
167
|
+
}
|
|
@@ -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 { useState } from "react";
|
|
22
|
+
import { useTranslations } from "next-intl";
|
|
23
|
+
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
|
24
|
+
import { Label } from "@/components/ui/label";
|
|
25
|
+
import { cn } from "@/lib/utils";
|
|
26
|
+
import { Monitor, Moon, Sun } from "lucide-react";
|
|
27
|
+
|
|
28
|
+
type Theme = "light" | "dark" | "system";
|
|
29
|
+
|
|
30
|
+
export function AppearanceForm() {
|
|
31
|
+
const [theme, setTheme] = useState<Theme>("system");
|
|
32
|
+
const t = useTranslations("settings.appearance");
|
|
33
|
+
|
|
34
|
+
const themes: { value: Theme; label: string; icon: React.ElementType }[] = [
|
|
35
|
+
{ value: "light", label: t("themeLight"), icon: Sun },
|
|
36
|
+
{ value: "dark", label: t("themeDark"), icon: Moon },
|
|
37
|
+
{ value: "system", label: t("themeSystem"), icon: Monitor },
|
|
38
|
+
];
|
|
39
|
+
|
|
40
|
+
return (
|
|
41
|
+
<Card>
|
|
42
|
+
<CardHeader>
|
|
43
|
+
<CardTitle>{t("cardTitle")}</CardTitle>
|
|
44
|
+
<CardDescription>{t("cardDescription")}</CardDescription>
|
|
45
|
+
</CardHeader>
|
|
46
|
+
<CardContent>
|
|
47
|
+
<div className="space-y-4">
|
|
48
|
+
<Label>{t("themeLabel")}</Label>
|
|
49
|
+
<div className="grid grid-cols-3 gap-4">
|
|
50
|
+
{themes.map((themeOption) => (
|
|
51
|
+
<button
|
|
52
|
+
key={themeOption.value}
|
|
53
|
+
onClick={() => setTheme(themeOption.value)}
|
|
54
|
+
className={cn(
|
|
55
|
+
"flex flex-col items-center gap-2 rounded-lg border-2 p-4 transition-colors hover:bg-muted",
|
|
56
|
+
theme === themeOption.value ? "border-primary" : "border-transparent"
|
|
57
|
+
)}
|
|
58
|
+
>
|
|
59
|
+
<themeOption.icon className="h-6 w-6" />
|
|
60
|
+
<span className="text-sm font-medium">{themeOption.label}</span>
|
|
61
|
+
</button>
|
|
62
|
+
))}
|
|
63
|
+
</div>
|
|
64
|
+
<p className="text-xs text-muted-foreground">
|
|
65
|
+
{t("themeComingSoon")}
|
|
66
|
+
</p>
|
|
67
|
+
</div>
|
|
68
|
+
</CardContent>
|
|
69
|
+
</Card>
|
|
70
|
+
);
|
|
71
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
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 { SettingsSidebar } from "./settings-sidebar";
|
|
19
|
+
|
|
20
|
+
export { ProfileForm } from "./profile-form";
|
|
21
|
+
|
|
22
|
+
export { AppearanceForm } from "./appearance-form";
|
|
23
|
+
export { ApiKeysList } from "./api-keys-list";
|
|
24
|
+
|
|
25
|
+
|