From 81a7e5d62ec29ad3bf971610d5243a11a62a603d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Carl-Gerhard=20Lindesv=C3=A4rd?= <1987198+lindesvard@users.noreply.github.com> Date: Thu, 16 Oct 2025 12:27:44 +0200 Subject: [PATCH] feat: dashboard v2, esm, upgrades (#211) * esm * wip * wip * wip * wip * wip * wip * subscription notice * wip * wip * wip * fix envs * fix: update docker build * fix * esm/types * delete dashboard :D * add patches to dockerfiles * update packages + catalogs + ts * wip * remove native libs * ts * improvements * fix redirects and fetching session * try fix favicon * fixes * fix * order and resize reportds within a dashboard * improvements * wip * added userjot to dashboard * fix * add op * wip * different cache key * improve date picker * fix table * event details loading * redo onboarding completely * fix login * fix * fix * extend session, billing and improve bars * fix * reduce price on 10M --- .github/workflows/docker-build.yml | 26 +- .gitignore | 4 + apps/api/Dockerfile | 8 +- apps/api/package.json | 15 +- apps/api/scripts/get-bots.ts | 5 + apps/api/scripts/mock.ts | 6 +- apps/api/src/controllers/ai.controller.ts | 2 +- apps/api/src/controllers/misc.controller.ts | 420 +- .../controllers/oauth-callback.controller.tsx | 12 +- .../api/src/controllers/webhook.controller.ts | 9 +- apps/api/src/hooks/request-logging.hook.ts | 2 +- apps/api/src/index.ts | 15 +- apps/api/src/routes/misc.router.ts | 12 + apps/api/src/utils/parseUrlMeta.ts | 43 +- apps/api/tsdown.config.ts | 23 + apps/api/tsup.config.ts | 26 - apps/dashboard/.gitignore | 39 - apps/dashboard/.sentryclirc | 3 - apps/dashboard/Dockerfile | 103 - apps/dashboard/README.md | 1 - apps/dashboard/components.json | 16 - apps/dashboard/entrypoint.sh | 33 - apps/dashboard/next.config.mjs | 47 - apps/dashboard/package.json | 149 - apps/dashboard/postcss.config.cjs | 8 - .../[projectId]/chat/page.tsx | 31 - .../dashboards/[dashboardId]/list-reports.tsx | 181 - .../dashboards/[dashboardId]/page.tsx | 32 - .../dashboards/list-dashboards/header.tsx | 22 - .../dashboards/list-dashboards/index.tsx | 25 - .../[projectId]/dashboards/page.tsx | 11 - .../[projectId]/events/conversions.tsx | 43 - .../[projectId]/events/events.tsx | 93 - .../[projectId]/events/page.tsx | 49 - .../[projectId]/layout-content.tsx | 33 - .../[projectId]/layout-menu.tsx | 243 - .../layout-organization-selector.tsx | 43 - .../[projectId]/layout-sidebar.tsx | 90 - .../layout-sticky-below-header.tsx | 22 - .../[organizationSlug]/[projectId]/layout.tsx | 66 - .../[projectId]/page-layout.tsx | 15 - .../[projectId]/pages/page.tsx | 34 - .../[projectId]/pages/pages.tsx | 193 - .../[profileId]/most-events/index.tsx | 20 - .../[projectId]/profiles/[profileId]/page.tsx | 80 - .../[profileId]/popular-routes/index.tsx | 20 - .../[profileId]/profile-activity/index.tsx | 20 - .../profile-activity/profile-activity.tsx | 164 - .../profiles/[profileId]/profile-charts.tsx | 106 - .../profiles/[profileId]/profile-events.tsx | 55 - .../[profileId]/profile-metrics/index.tsx | 18 - .../profile-metrics/profile-metrics.tsx | 123 - .../[projectId]/profiles/page.tsx | 44 - .../[projectId]/profiles/power-users.tsx | 41 - .../profiles/profile-last-seen/index.tsx | 68 - .../[projectId]/profiles/profiles.tsx | 51 - .../[projectId]/realtime/map/coordinates.ts | 222 - .../[projectId]/realtime/map/index.tsx | 20 - .../[projectId]/realtime/map/map.tsx | 185 - .../[projectId]/realtime/page.tsx | 145 - .../realtime/realtime-live-events/index.tsx | 21 - .../realtime-live-events/live-events.tsx | 46 - .../realtime/realtime-reloader.tsx | 35 - .../[projectId]/reports/[reportId]/page.tsx | 29 - .../[projectId]/reports/page.tsx | 13 - .../retention/last-active-users/chart.tsx | 103 - .../retention/last-active-users/index.tsx | 25 - .../[projectId]/retention/page.tsx | 65 - .../retention/rolling-active-users/chart.tsx | 146 - .../retention/rolling-active-users/index.tsx | 35 - .../users-retention-series/chart.tsx | 117 - .../users-retention-series/index.tsx | 25 - .../retention/weekly-cohorts/index.tsx | 132 - .../settings/integrations/page.tsx | 36 - .../[projectId]/settings/loading.tsx | 3 - .../settings/notifications/page.tsx | 40 - .../settings/organization/invites/index.tsx | 28 - .../settings/organization/loading.tsx | 3 - .../settings/organization/members/index.tsx | 18 - .../organization/organization/billing.tsx | 302 - .../organization/organization/usage.tsx | 288 - .../settings/organization/page.tsx | 107 - .../[projectId]/settings/page.tsx | 1 - .../settings/profile/edit-profile.tsx | 89 - .../[projectId]/settings/profile/loading.tsx | 3 - .../[projectId]/settings/profile/logout.tsx | 18 - .../[projectId]/settings/profile/page.tsx | 17 - .../[projectId]/settings/projects/loading.tsx | 3 - .../[projectId]/settings/projects/page.tsx | 42 - .../settings/projects/project-clients.tsx | 45 - .../settings/references/list-references.tsx | 29 - .../settings/references/loading.tsx | 3 - .../[projectId]/settings/references/page.tsx | 25 - .../[projectId]/side-effects-free-plan.tsx | 61 - .../[projectId]/side-effects-timezone.tsx | 91 - .../[projectId]/side-effects-trial.tsx | 67 - .../[projectId]/side-effects.tsx | 41 - .../src/app/(app)/[organizationSlug]/page.tsx | 65 - apps/dashboard/src/app/(app)/page.tsx | 15 - apps/dashboard/src/app/(auth)/layout.tsx | 22 - .../src/app/(auth)/live-events/index.tsx | 7 - apps/dashboard/src/app/(auth)/login/page.tsx | 64 - .../src/app/(auth)/reset-password/page.tsx | 17 - .../dashboard/src/app/(onboarding)/layout.tsx | 42 - .../app/(onboarding)/onboarding-layout.tsx | 42 - .../connect/onboarding-connect.tsx | 70 - .../onboarding/[projectId]/connect/page.tsx | 28 - .../[projectId]/verify/onboarding-verify.tsx | 155 - .../onboarding/[projectId]/verify/page.tsx | 41 - .../src/app/(onboarding)/onboarding/page.tsx | 78 - .../(onboarding)/onboarding/project/page.tsx | 11 - .../app/(public)/share/overview/[id]/page.tsx | 87 - .../src/app/api/healthcheck/route.tsx | 5 - apps/dashboard/src/app/favicon.ico | Bin 15406 -> 0 bytes apps/dashboard/src/app/global-error.tsx | 20 - apps/dashboard/src/app/layout.tsx | 48 - apps/dashboard/src/app/maintenance/page.tsx | 18 - apps/dashboard/src/app/manifest.ts | 16 - apps/dashboard/src/app/providers.tsx | 90 - apps/dashboard/src/app/robots.txt | 2 - apps/dashboard/src/components/auth/or.tsx | 11 - .../components/auth/reset-password-form.tsx | 57 - .../src/components/clients/client-actions.tsx | 73 - .../src/components/clients/table/columns.tsx | 56 - .../src/components/clients/table/index.tsx | 67 - .../src/components/dark-mode-toggle.tsx | 43 - apps/dashboard/src/components/data-table.tsx | 120 - .../components/events/event-field-value.tsx | 77 - .../events/table/events-data-table.tsx | 160 - .../events/table/events-table-columns.tsx | 70 - .../src/components/events/table/index.tsx | 128 - apps/dashboard/src/components/links.tsx | 30 - .../notifications/notification-provider.tsx | 29 - .../notifications/notifications.tsx | 14 - .../components/notifications/table/index.tsx | 63 - .../overview/live-counter/index.tsx | 13 - .../overview/live-counter/live-counter.tsx | 85 - .../overview/overview-hydrate-options.tsx | 22 - .../overview/overview-live-histogram.tsx | 140 - .../overview/overview-share/index.tsx | 19 - .../components/overview/overview-top-bots.tsx | 80 - .../overview/overview-top-events/index.tsx | 16 - apps/dashboard/src/components/page-tabs.tsx | 75 - apps/dashboard/src/components/pagination.tsx | 82 - .../src/components/profiles/table/index.tsx | 74 - .../src/components/projects/project-card.tsx | 114 - .../src/components/references/table.tsx | 27 - .../src/components/settings-toggle.tsx | 113 - .../components/settings/invites/columns.tsx | 126 - .../src/components/settings/invites/index.tsx | 46 - .../components/settings/members/columns.tsx | 161 - .../src/components/settings/members/index.tsx | 46 - .../src/components/sign-out-button.tsx | 17 - apps/dashboard/src/components/ui/badge.tsx | 42 - apps/dashboard/src/components/ui/button.tsx | 180 - apps/dashboard/src/components/ui/calendar.tsx | 63 - apps/dashboard/src/components/ui/command.tsx | 151 - apps/dashboard/src/components/ui/dialog.tsx | 120 - .../src/components/ui/scroll-area.tsx | 56 - apps/dashboard/src/components/ui/table.tsx | 155 - apps/dashboard/src/env.mjs | 47 - .../src/hocs/with-loading-widget.tsx | 37 - apps/dashboard/src/hocs/with-suspense.tsx | 22 - apps/dashboard/src/hooks/useAppParams.ts | 15 - apps/dashboard/src/hooks/useAuth.tsx | 5 - apps/dashboard/src/hooks/useCursor.ts | 34 - apps/dashboard/src/hooks/useEventNames.ts | 10 - .../dashboard/src/hooks/useEventProperties.ts | 15 - apps/dashboard/src/hooks/useLogout.ts | 15 - apps/dashboard/src/hooks/useMappings.ts | 13 - .../src/hooks/useProfileProperties.ts | 9 - apps/dashboard/src/hooks/useProfileValues.ts | 15 - apps/dashboard/src/hooks/usePropertyValues.ts | 11 - apps/dashboard/src/instrumentation.ts | 10 - apps/dashboard/src/mappings.json | 6 - apps/dashboard/src/middleware.ts | 100 - apps/dashboard/src/modals/SaveReport.tsx | 150 - apps/dashboard/src/modals/Testimonial.tsx | 78 - apps/dashboard/src/modals/index.tsx | 98 - apps/dashboard/src/styles/globals.css | 171 - apps/dashboard/src/trpc/client.tsx | 40 - apps/dashboard/src/utils/casing.ts | 5 - apps/dashboard/src/utils/getters.ts | 27 - apps/dashboard/src/utils/meta.ts | 21 - apps/dashboard/src/utils/theme.ts | 17 - apps/dashboard/tailwind.config.js | 193 - apps/dashboard/tsconfig.json | 18 - apps/docs/Dockerfile | 11 - apps/docs/index.js | 43 - apps/docs/package.json | 12 - apps/public/app/docs/layout.tsx | 4 +- .../content/docs/self-hosting/changelog.mdx | 2 +- apps/public/lib/utils.ts | 2 +- apps/public/package.json | 12 +- apps/public/postcss.config.js | 2 +- apps/public/tailwind.config.js | 3 +- apps/start/.cta.json | 20 + apps/start/.cursorrules | 337 + apps/start/.gitignore | 12 + apps/start/.vscode/settings.json | 41 + apps/start/README.md | 310 + apps/start/ROUTING.md | 118 + apps/start/biome.json | 31 + apps/start/components.json | 21 + apps/start/package.json | 174 + apps/{dashboard => start}/public/favicon.ico | Bin apps/start/public/img-1.png | Bin 0 -> 2060624 bytes apps/start/public/img-2.png | Bin 0 -> 1644607 bytes apps/start/public/img-3.png | Bin 0 -> 1804691 bytes apps/start/public/img-4.png | Bin 0 -> 2099337 bytes apps/start/public/img-5.png | Bin 0 -> 1935783 bytes apps/start/public/img-6.png | Bin 0 -> 1716048 bytes apps/{dashboard => start}/public/logo.svg | 0 apps/start/public/logo192.png | Bin 0 -> 5347 bytes apps/start/public/logo512.png | Bin 0 -> 9664 bytes apps/start/public/manifest.json | 25 + apps/start/public/robots.txt | 3 + apps/start/src/app/global-middleware.ts | 10 + .../src/components/animate-height.tsx | 0 apps/start/src/components/animated-number.tsx | 20 + apps/start/src/components/auth/or.tsx | 11 + .../components/auth/reset-password-form.tsx | 68 + .../components/auth/share-enter-password.tsx | 27 +- .../components/auth/sign-in-email-form.tsx | 43 +- .../src/components/auth/sign-in-github.tsx | 32 +- .../src/components/auth/sign-in-google.tsx | 32 +- .../components/auth/sign-up-email-form.tsx | 48 +- .../src/components/button-container.tsx | 0 .../src/components/card.tsx | 2 - .../src/components/chart-ssr.tsx | 22 +- .../src/components/charts/chart-tooltip.tsx | 0 .../src/components/charts/common-bar.tsx | 47 + .../src/components/chat/chat-form.tsx | 0 .../src/components/chat/chat-message.tsx | 0 .../src/components/chat/chat-messages.tsx | 4 - .../src/components/chat/chat-report.tsx | 2 - .../src/components/chat/chat.tsx | 12 +- .../src/components/click-to-copy.tsx | 2 - .../clients/create-client-success.tsx | 2 - .../src/components/clients/table/columns.tsx | 91 + .../src/components/clients/table/index.tsx | 30 + .../src/components/color-square.tsx | 0 .../src/components/dot.tsx | 0 .../src/components/events/event-icon.tsx | 0 .../src/components/events/event-list-item.tsx | 37 +- .../src/components/events/event-listener.tsx | 55 +- .../events/list-properties-icon.tsx | 0 .../src/components/events/table/columns.tsx | 21 +- .../src/components/events/table/index.tsx | 298 + .../src/components/events/table/item.tsx | 178 + .../src/components/fade-in.tsx | 2 - apps/start/src/components/feedback-button.tsx | 27 + .../src/components/forms/checkbox-item.tsx | 0 .../src/components/forms/copy-input.tsx | 2 +- .../src/components/forms/input-with-label.tsx | 0 .../src/components/forms/tag-input.tsx | 2 +- .../src/components/full-page-empty-state.tsx | 5 +- .../src/components/full-page-error-state.tsx | 20 + .../components/full-page-loading-state.tsx | 9 +- .../src/components/full-width-navbar.tsx | 4 +- .../src/components/fullscreen-toggle.tsx | 6 +- .../src/components/grid-table.tsx | 0 .../integrations/active-integrations.tsx | 39 +- .../integrations/all-integrations.tsx | 4 +- .../forms/discord-integration.tsx | 21 +- .../integrations/forms/slack-integration.tsx | 28 +- .../forms/webhook-integration.tsx | 24 +- .../integrations/integration-card.tsx | 0 .../components/integrations/integrations.tsx | 0 apps/start/src/components/lazy-component.tsx | 79 + apps/start/src/components/links.tsx | 36 + .../start/src/components/login-left-panel.tsx | 104 + .../src/components/logo.tsx | 0 .../src/components/markdown.tsx | 0 .../src/components/mock-event-list.tsx} | 8 +- .../notifications/notification-provider.tsx | 26 + .../notifications/notification-rules.tsx | 24 +- .../notifications/notifications.tsx | 16 + .../components/notifications/rule-card.tsx | 26 +- .../notifications/table/columns.tsx | 52 +- .../components/notifications/table/index.tsx | 32 + .../src/components/onboarding-left-panel.tsx | 132 + .../components/onboarding}/connect-app.tsx | 11 +- .../onboarding}/connect-backend.tsx | 24 +- .../components/onboarding}/connect-web.tsx | 16 +- .../components/onboarding/curl-preview.tsx | 72 + .../onboarding-verify-listener.tsx | 33 +- .../onboarding}/skip-onboarding.tsx | 47 +- .../src/components/onboarding}/steps.tsx | 14 +- .../components}/organization/billing-faq.tsx | 4 +- .../src/components/organization/billing.tsx | 432 + .../organization/current-subscription.tsx | 107 +- .../organization/edit-organization.tsx | 35 +- .../components}/organization/organization.tsx | 0 .../src/components/organization/usage.tsx | 374 + .../overview/filters/origin-filter.tsx | 24 +- .../filters/overview-filters-buttons.tsx | 6 +- .../overview-filters-drawer-content.tsx | 14 +- .../filters/overview-filters-drawer.tsx | 2 - .../src/components/overview/live-counter.tsx | 81 + .../overview/overview-chart-toggle.tsx | 0 .../overview/overview-constants.tsx | 0 .../overview/overview-details-button.tsx | 0 .../components/overview/overview-interval.tsx | 4 - .../overview/overview-live-histogram.tsx | 251 + .../overview/overview-metric-card.tsx | 26 +- .../components/overview/overview-metrics.tsx | 244 +- .../components/overview/overview-range.tsx | 2 - .../components/overview}/overview-share.tsx | 44 +- .../overview/overview-top-devices.tsx | 31 +- .../overview}/overview-top-events.tsx | 28 +- .../overview/overview-top-generic-modal.tsx | 48 +- .../components/overview/overview-top-geo.tsx | 32 +- .../overview/overview-top-pages-modal.tsx | 46 +- .../overview/overview-top-pages.tsx | 29 +- .../overview/overview-top-sources.tsx | 28 +- .../overview/overview-widget-table.tsx | 6 +- .../components/overview/overview-widget.tsx | 51 +- .../components/overview/useOverviewOptions.ts | 4 +- .../components/overview/useOverviewWidget.tsx | 0 apps/start/src/components/page-container.tsx | 18 + apps/start/src/components/page-header.tsx | 25 + .../src/components/pagination-floating.tsx | 11 + apps/start/src/components/pagination.tsx | 106 + .../src/components/ping.tsx | 0 apps/start/src/components/profile-toggle.tsx | 70 + .../src/components/profiles/latest-events.tsx | 74 + .../src/components/profiles}/most-events.tsx | 12 +- .../components/profiles}/popular-routes.tsx | 12 +- .../components/profiles/profile-activity.tsx | 92 + .../components/profiles/profile-avatar.tsx | 18 +- .../components/profiles/profile-charts.tsx | 105 + .../components/profiles/profile-metrics.tsx | 110 + .../profiles/profile-properties.tsx | 114 + .../src/components/profiles/table/columns.tsx | 17 +- .../src/components/profiles/table/index.tsx | 94 + .../src/components/project-selector.tsx} | 70 +- .../src/components/projects/project-card.tsx | 155 + apps/start/src/components/providers.tsx | 41 + .../react-virtualized-auto-sizer.tsx | 0 .../components/realtime/map/coordinates.ts | 397 + .../src/components/realtime/map/index.tsx | 352 + .../components}/realtime/map/map.helpers.tsx | 2 +- .../src/components}/realtime/map/markers.ts | 19 +- .../realtime/realtime-active-sessions.tsx | 87 + .../src/components/realtime/realtime-geo.tsx | 95 + .../realtime/realtime-live-histogram.tsx | 55 +- .../components/realtime/realtime-paths.tsx | 102 + .../realtime/realtime-referrals.tsx | 85 + .../components/realtime/realtime-reloader.tsx | 40 + .../components/report-chart/area/chart.tsx | 32 +- .../components/report-chart/area/index.tsx | 18 +- .../report-chart/aspect-container.tsx | 2 - .../src/components/report-chart/bar/chart.tsx | 140 +- .../src/components/report-chart/bar/index.tsx | 18 +- .../components/report-chart/common/axis.tsx | 22 +- .../components/report-chart/common/empty.tsx | 0 .../components/report-chart/common/error.tsx | 0 .../report-chart/common/linear-gradient.tsx | 0 .../report-chart/common/loading.tsx | 0 .../common/previous-diff-indicator.tsx | 8 +- .../common/report-chart-tooltip.tsx | 6 +- .../report-chart/common/report-table.tsx | 8 +- .../report-chart/common/serie-icon.flags.tsx | 0 .../report-chart/common/serie-icon.tsx | 22 +- .../report-chart/common/serie-icon.urls.ts | 6 + .../report-chart/common/serie-name.tsx | 0 .../src/components/report-chart/context.tsx | 0 .../report-chart/conversion/chart.tsx | 32 +- .../report-chart/conversion/index.tsx | 18 +- .../report-chart/conversion/summary.tsx | 3 +- .../components/report-chart/funnel/chart.tsx | 13 +- .../components/report-chart/funnel/index.tsx | 17 +- .../report-chart/histogram/chart.tsx | 52 +- .../report-chart/histogram/index.tsx | 18 +- .../src/components/report-chart/index.tsx | 14 +- .../components/report-chart/line/chart.tsx | 128 +- .../components/report-chart/line/index.tsx | 18 +- .../src/components/report-chart/map/chart.tsx | 8 +- .../src/components/report-chart/map/index.tsx | 18 +- .../components/report-chart/metric/chart.tsx | 4 +- .../components/report-chart/metric/index.tsx | 18 +- .../report-chart/metric/metric-card.tsx | 4 +- .../src/components/report-chart/pie/chart.tsx | 2 +- .../src/components/report-chart/pie/index.tsx | 18 +- .../report-chart}/report-editor.tsx | 9 +- .../report-chart/retention/chart.tsx | 2 - .../report-chart/retention/index.tsx | 42 +- .../report-chart/retention/table.tsx | 2 +- .../report-chart/retention/tooltip.tsx | 2 +- .../src/components/report-chart/shortcut.tsx | 0 .../src/components/report/ReportChartType.tsx | 0 .../src/components/report/ReportInterval.tsx | 0 .../src/components/report/ReportLineType.tsx | 0 .../components/report/ReportSaveButton.tsx | 42 +- .../src/components/report/ReportSegment.tsx | 0 .../components/report/edit-report-name.tsx | 2 - .../src/components/report/reportSlice.ts | 0 .../sidebar/EventPropertiesCombobox.tsx | 4 +- .../report/sidebar/PropertiesCombobox.tsx | 4 +- .../report/sidebar/ReportBreakdownMore.tsx | 0 .../report/sidebar/ReportBreakdowns.tsx | 6 +- .../report/sidebar/ReportEventMore.tsx | 0 .../report/sidebar/ReportEvents.tsx | 8 +- .../report/sidebar/ReportFormula.tsx | 2 - .../report/sidebar/ReportSettings.tsx | 2 - .../report/sidebar/ReportSidebar.tsx | 0 .../report/sidebar/filters/FilterItem.tsx | 16 +- .../report/sidebar/filters/FiltersList.tsx | 0 apps/start/src/components/selling-points.tsx | 49 + .../src/components/sessions/table/columns.tsx | 271 + .../src/components/sessions/table/index.tsx | 304 + .../components/settings}/delete-project.tsx | 50 +- .../settings}/edit-project-details.tsx | 27 +- .../settings}/edit-project-filters.tsx | 31 +- .../components/settings/invites/columns.tsx | 141 + .../src/components/settings/invites/index.tsx | 41 + .../components/settings/members/columns.tsx | 136 + .../src/components/settings/members/index.tsx | 31 + apps/start/src/components/sidebar-link.tsx | 32 + .../components/sidebar-organization-menu.tsx | 174 + .../src/components/sidebar-project-menu.tsx | 171 + apps/start/src/components/sidebar.tsx | 151 + .../src/components/skeleton-dashboard.tsx | 130 + .../src/components/skeleton.tsx | 0 .../src/components/stats.tsx | 0 .../src/components/syntax.tsx | 2 - apps/start/src/components/theme-provider.tsx | 164 + .../src/components/time-window-picker.tsx | 0 .../src/components/tooltip-complete.tsx | 0 .../src/components/ui/RenderDots.tsx | 2 - .../src/components/ui/accordion.tsx | 0 .../src/components/ui/alert-dialog.tsx | 2 - .../src/components/ui/alert.tsx | 0 .../src/components/ui/aspect-ratio.tsx | 2 - .../src/components/ui/avatar.tsx | 2 - apps/start/src/components/ui/badge.tsx | 46 + apps/start/src/components/ui/button.tsx | 219 + apps/start/src/components/ui/calendar.tsx | 215 + .../src/components/ui/carousel.tsx | 10 +- .../src/components/ui/checkbox.tsx | 2 - .../src/components/ui/combobox-advanced.tsx | 47 +- .../src/components/ui/combobox-events.tsx | 8 +- .../src/components/ui/combobox.tsx | 2 - apps/start/src/components/ui/command.tsx | 184 + .../data-table/data-table-column-header.tsx | 104 + .../ui/data-table/data-table-config.ts | 82 + .../ui/data-table/data-table-date-filter.tsx | 224 + .../data-table/data-table-faceted-filter.tsx | 193 + .../ui/data-table/data-table-helpers.tsx | 77 + .../ui/data-table/data-table-hooks.tsx | 34 + .../ui/data-table/data-table-parsers.ts | 99 + .../data-table/data-table-slider-filter.tsx | 239 + .../ui/data-table/data-table-toolbar.tsx | 237 + .../ui/data-table/data-table-view-options.tsx | 87 + .../components/ui/data-table/data-table.tsx | 138 + .../components/ui/data-table/use-table.tsx | 227 + .../ui/data-table/virtualized-data-table.tsx | 181 + apps/start/src/components/ui/date-time.tsx | 121 + apps/start/src/components/ui/dialog.tsx | 146 + .../src/components/ui/dropdown-menu.tsx | 11 +- .../src/components/ui/gradient-background.tsx | 0 .../src/components/ui/input-date-time.tsx | 88 + .../src/components/ui/input-enter.tsx | 2 +- .../src/components/ui/input-otp.tsx | 0 .../src/components/ui/input-with-toggle.tsx | 0 .../src/components/ui/input.tsx | 4 +- .../src/components/ui/key-value-grid.tsx | 221 + .../src/components/ui/key-value.tsx | 2 +- .../src/components/ui/label.tsx | 4 +- .../src/components/ui/padding.tsx | 0 .../src/components/ui/popover.tsx | 4 +- .../src/components/ui/progress.tsx | 0 .../src/components/ui/radio-group.tsx | 0 apps/start/src/components/ui/scroll-area.tsx | 61 + apps/start/src/components/ui/select.tsx | 185 + apps/start/src/components/ui/separator.tsx | 26 + .../src/components/ui/sheet.tsx | 2 - apps/start/src/components/ui/slider.tsx | 63 + .../src/components/ui/sonner.tsx | 2 +- apps/start/src/components/ui/spinner.tsx | 301 + .../src/components/ui/switch.tsx | 2 - apps/start/src/components/ui/table.tsx | 128 + .../src/components/ui/tabs.tsx | 10 +- .../src/components/ui/textarea.tsx | 0 .../src/components/ui/toast.tsx | 2 - .../src/components/ui/toaster.tsx | 4 +- .../src/components/ui/toggle-group.tsx | 0 .../src/components/ui/toggle.tsx | 0 .../src/components/ui/tooltip.tsx | 2 - .../src/components/widget-table.tsx | 0 .../src/components/widget.tsx | 0 apps/start/src/hooks/use-app-context.ts | 16 + apps/start/src/hooks/use-app-params.ts | 11 + .../src/hooks/use-breakpoint.ts} | 9 +- apps/start/src/hooks/use-callback-ref.ts | 27 + .../src/hooks/use-client-secret.ts} | 0 .../src/hooks/use-dashed-stroke.tsx | 0 .../src/hooks/use-debounce-fn.ts} | 4 +- .../src/hooks/use-debounce-state.ts} | 4 +- .../src/hooks/use-debounce-value.ts} | 0 .../start/src/hooks/use-debounced-callback.ts | 28 + apps/start/src/hooks/use-event-names.ts | 13 + apps/start/src/hooks/use-event-properties.ts | 21 + .../src/hooks/use-event-query-filters.ts} | 0 .../src/hooks/use-format-date-interval.ts} | 0 apps/start/src/hooks/use-logout.ts | 14 + .../src/hooks/use-numer-formatter.ts} | 5 + apps/start/src/hooks/use-page-tabs.ts | 27 + .../start/src/hooks/use-profile-properties.ts | 12 + apps/start/src/hooks/use-profile-values.ts | 13 + apps/start/src/hooks/use-property-values.ts | 12 + .../src/hooks/use-rechart-data-model.ts} | 2 - .../src/hooks/use-scroll-anchor.ts | 0 .../start/src/hooks/use-search-query-state.ts | 32 + apps/start/src/hooks/use-session-extension.ts | 35 + apps/start/src/hooks/use-theme.ts | 1 + .../src/hooks/use-throttle.ts} | 0 .../src/hooks/use-visible-series.ts} | 2 - .../useWS.ts => start/src/hooks/use-ws.ts} | 15 +- .../integrations/tanstack-query/devtools.tsx | 6 + .../tanstack-query/root-provider.tsx | 86 + apps/start/src/integrations/trpc/react.ts | 21 + apps/start/src/lib/utils.ts | 6 + apps/start/src/logo.svg | 44 + .../src/modals/Instructions.tsx | 0 .../src/modals/Modal/Container.tsx | 12 +- .../src/modals/add-client.tsx} | 36 +- .../src/modals/add-dashboard.tsx} | 41 +- .../src/modals/add-integration.tsx | 32 +- .../src/modals/add-notification-rule.tsx | 51 +- .../src/modals/add-project.tsx | 40 +- .../src/modals/add-reference.tsx} | 46 +- .../src/modals/confirm.tsx} | 0 .../src/modals}/create-invite.tsx | 63 +- .../src/modals/date-ranger-picker.tsx} | 56 +- apps/start/src/modals/date-time-picker.tsx | 193 + .../src/modals/edit-client.tsx} | 33 +- .../src/modals/edit-dashboard.tsx} | 32 +- .../src/modals/edit-event.tsx | 58 +- apps/start/src/modals/edit-member.tsx | 86 + apps/start/src/modals/edit-reference.tsx | 96 + .../src/modals/edit-report.tsx} | 0 .../src/modals/event-details.tsx | 314 +- apps/start/src/modals/index.tsx | 69 + .../src/modals/onboarding-troubleshoot.tsx} | 0 .../src/modals/overview-chart-details.tsx} | 0 .../src/modals/request-reset-password.tsx | 27 +- apps/start/src/modals/save-report.tsx | 299 + .../src/modals/share-overview-modal.tsx} | 43 +- apps/{dashboard => start}/src/redux/index.ts | 0 apps/start/src/routeTree.gen.ts | 1647 ++ apps/start/src/router.tsx | 40 + apps/start/src/routes/__root.tsx | 249 + .../_app.$organizationId.$projectId.tsx} | 43 +- .../_app.$organizationId.$projectId_.chat.tsx | 54 + ...organizationId.$projectId_.dashboards.tsx} | 118 +- ...d.$projectId_.dashboards_.$dashboardId.tsx | 580 + ...d.$projectId_.events._tabs.conversions.tsx | 35 + ...tionId.$projectId_.events._tabs.events.tsx | 41 + ...ationId.$projectId_.events._tabs.index.tsx | 17 + ...tionId.$projectId_.events._tabs.stats.tsx} | 19 +- ...rganizationId.$projectId_.events._tabs.tsx | 62 + ....$projectId_.notifications._tabs.index.tsx | 17 + ...tId_.notifications._tabs.notifications.tsx | 31 + ....$projectId_.notifications._tabs.rules.tsx | 99 + ...tionId.$projectId_.notifications._tabs.tsx | 61 + ..._app.$organizationId.$projectId_.pages.tsx | 282 + ...ctId_.profiles.$profileId._tabs.events.tsx | 41 + ...ectId_.profiles.$profileId._tabs.index.tsx | 140 + ....$projectId_.profiles.$profileId._tabs.tsx | 124 + ...d.$projectId_.profiles._tabs.anonymous.tsx | 45 + ....$projectId_.profiles._tabs.identified.tsx | 47 + ...ionId.$projectId_.profiles._tabs.index.tsx | 17 + ...$projectId_.profiles._tabs.power-users.tsx | 41 + ...anizationId.$projectId_.profiles._tabs.tsx | 62 + ...p.$organizationId.$projectId_.realtime.tsx | 84 + ...$organizationId.$projectId_.references.tsx | 188 + ...pp.$organizationId.$projectId_.reports.tsx | 26 + ...ationId.$projectId_.reports_.$reportId.tsx | 40 + ...p.$organizationId.$projectId_.sessions.tsx | 58 + ...ionId.$projectId_.sessions_.$sessionId.tsx | 124 + ...nId.$projectId_.settings._tabs.clients.tsx | 19 + ...nId.$projectId_.settings._tabs.details.tsx | 36 + ...onId.$projectId_.settings._tabs.events.tsx | 30 + ...ionId.$projectId_.settings._tabs.index.tsx | 17 + ...anizationId.$projectId_.settings._tabs.tsx | 73 + .../routes/_app.$organizationId.billing.tsx | 64 + .../src/routes/_app.$organizationId.index.tsx | 97 + ...izationId.integrations._tabs.available.tsx | 12 + ...rganizationId.integrations._tabs.index.tsx | 18 + ...izationId.integrations._tabs.installed.tsx | 12 + ...app.$organizationId.integrations._tabs.tsx | 73 + ...pp.$organizationId.members._tabs.index.tsx | 15 + ...ganizationId.members._tabs.invitations.tsx | 23 + ....$organizationId.members._tabs.members.tsx | 21 + .../_app.$organizationId.members._tabs.tsx | 54 + .../routes/_app.$organizationId.settings.tsx | 134 + .../start/src/routes/_app.$organizationId.tsx | 162 + apps/start/src/routes/_app.tsx | 24 + apps/start/src/routes/_login.login.tsx | 73 + .../src/routes/_login.reset-password.tsx | 28 + apps/start/src/routes/_login.tsx | 25 + apps/start/src/routes/_public.onboarding.tsx | 126 + apps/start/src/routes/_public.tsx | 19 + .../_steps.onboarding.$projectId.connect.tsx | 90 + .../_steps.onboarding.$projectId.verify.tsx | 110 + .../src/routes/_steps.onboarding.project.tsx} | 125 +- apps/start/src/routes/_steps.tsx | 75 + apps/start/src/routes/index.tsx | 99 + .../src/routes/share.overview.$shareId.tsx | 107 + apps/start/src/server/get-envs.ts | 19 + apps/start/src/styles.css | 330 + .../src/translations/countries.ts | 0 .../src/translations/properties.ts | 0 apps/start/src/trpc/client.ts | 16 + apps/start/src/types/data-table.ts | 46 + apps/{dashboard => start}/src/types/index.ts | 0 .../src/types/react-simple-map.d.ts | 0 apps/start/src/utils/are-props-equal.ts | 12 + apps/start/src/utils/casing.ts | 9 + .../src/utils/clipboard.ts | 0 apps/{dashboard => start}/src/utils/cn.ts | 0 apps/{dashboard => start}/src/utils/date.ts | 38 +- .../{dashboard => start}/src/utils/getDbId.ts | 0 apps/start/src/utils/getters.ts | 33 + apps/{dashboard => start}/src/utils/math.ts | 0 apps/start/src/utils/op.ts | 8 + .../src/utils/should-ignore-keypress.ts | 6 +- apps/{dashboard => start}/src/utils/slug.ts | 0 .../{dashboard => start}/src/utils/storage.ts | 0 apps/start/src/utils/theme.ts | 33 + apps/start/src/utils/title.ts | 125 + .../src/utils/truncate.ts | 0 apps/start/tsconfig.json | 28 + apps/start/vite.config.ts | 28 + apps/start/wrangler.jsonc | 20 + apps/worker/Dockerfile | 4 +- apps/worker/package.json | 9 +- apps/worker/scripts/get-referrers.ts | 5 + apps/worker/src/boot-cron.ts | 3 +- apps/worker/src/index.ts | 53 +- apps/worker/src/jobs/cron.delete-projects.ts | 6 +- apps/worker/src/jobs/cron.ping.ts | 3 +- apps/worker/tsdown.config.ts | 23 + apps/worker/tsup.config.ts | 19 - package.json | 14 +- packages/auth/constants.ts | 4 +- packages/auth/package.json | 9 +- packages/auth/parse-cookie-domain.ts | 11 - packages/cli/package.json | 5 +- packages/cli/src/importer/importer.ts | 2 +- packages/common/package.json | 5 +- packages/constants/package.json | 4 +- packages/db/code-migrations/2-accounts.ts | 5 + packages/db/code-migrations/3-init-ch.ts | 5 + packages/db/code-migrations/helpers.ts | 4 +- packages/db/code-migrations/migrate.ts | 5 + packages/db/package.json | 13 +- .../migration.sql | 29 + .../db/prisma/migrations/migration_lock.toml | 4 +- packages/db/prisma/prisma-json-types.ts | 210 + packages/db/prisma/schema.prisma | 38 +- packages/db/src/clickhouse/client.ts | 6 +- packages/db/src/clickhouse/query-builder.ts | 8 +- packages/db/src/generated/empty | 0 packages/db/src/prisma-client.ts | 22 +- packages/db/src/services/chart.service.ts | 87 +- packages/db/src/services/event.service.ts | 155 +- packages/db/src/services/funnel.service.ts | 4 +- .../db/src/services/organization.service.ts | 24 +- packages/db/src/services/profile.service.ts | 85 +- packages/db/src/services/project.service.ts | 6 +- packages/db/src/services/reference.service.ts | 16 - packages/db/src/services/reports.service.ts | 13 +- packages/db/src/services/retention.service.ts | 14 +- packages/db/src/services/session.service.ts | 297 +- packages/email/package.json | 13 +- packages/email/src/emails/email-invite.tsx | 2 +- .../email/src/emails/email-reset-password.tsx | 2 +- packages/geo/package.json | 4 +- packages/geo/scripts/download.ts | 5 + packages/geo/src/geo.ts | 5 + packages/integrations/package.json | 5 +- packages/integrations/src/slack.ts | 3 +- packages/json/package.json | 5 +- packages/logger/package.json | 3 +- packages/payments/package.json | 9 +- .../payments/scripts/create-custom-pricing.ts | 5 +- packages/payments/scripts/create-products.ts | 5 +- packages/payments/src/polar.ts | 11 +- packages/payments/src/prices.ts | 2 +- packages/payments/tsconfig.json | 20 +- packages/queue/package.json | 5 +- packages/redis/package.json | 7 +- packages/redis/redis.ts | 2 +- packages/sdks/_info/package.json | 6 +- .../sdks/astro/src/OpenPanelComponent.astro | 5 +- packages/sdks/express/package.json | 2 +- packages/sdks/nextjs/package.json | 4 +- packages/sdks/react-native/package.json | 4 +- packages/sdks/sdk/package.json | 4 +- packages/sdks/web/package.json | 4 +- packages/sdks/web/src/types.d.ts | 10 +- packages/trpc/package.json | 12 +- packages/trpc/src/root.ts | 8 +- packages/trpc/src/routers/auth.ts | 36 +- packages/trpc/src/routers/chart.helpers.ts | 8 +- packages/trpc/src/routers/chart.ts | 86 +- packages/trpc/src/routers/chat.ts | 20 + packages/trpc/src/routers/client.ts | 13 + packages/trpc/src/routers/dashboard.ts | 26 + packages/trpc/src/routers/event.ts | 77 +- packages/trpc/src/routers/notification.ts | 2 +- packages/trpc/src/routers/onboarding.ts | 9 +- packages/trpc/src/routers/organization.ts | 56 +- packages/trpc/src/routers/overview.ts | 6 + packages/trpc/src/routers/profile.ts | 103 +- packages/trpc/src/routers/project.ts | 23 +- packages/trpc/src/routers/realtime.ts | 143 + packages/trpc/src/routers/reference.ts | 67 +- packages/trpc/src/routers/report.ts | 135 +- packages/trpc/src/routers/session.ts | 64 + packages/trpc/src/routers/share.ts | 49 +- packages/trpc/src/routers/subscription.ts | 2 +- packages/trpc/src/routers/ticket.ts | 37 - packages/trpc/src/routers/user.ts | 6 + packages/trpc/src/trpc.ts | 32 +- packages/validation/package.json | 5 +- patches/nuqs.patch | 18 + pnpm-lock.yaml | 12712 +++++++++++----- pnpm-workspace.yaml | 6 + self-hosting/.env.template | 7 +- self-hosting/coolify.yml | 16 +- self-hosting/quiz.ts | 7 +- sh/docker-build | 4 +- tooling/publish/package.json | 4 +- tooling/publish/publish.ts | 5 + tooling/typescript/base.json | 12 +- tooling/typescript/package.json | 1 + tooling/unused-deps.mjs | 201 + 741 files changed, 32695 insertions(+), 16996 deletions(-) create mode 100644 apps/api/tsdown.config.ts delete mode 100644 apps/api/tsup.config.ts delete mode 100644 apps/dashboard/.gitignore delete mode 100644 apps/dashboard/.sentryclirc delete mode 100644 apps/dashboard/Dockerfile delete mode 100644 apps/dashboard/README.md delete mode 100644 apps/dashboard/components.json delete mode 100644 apps/dashboard/entrypoint.sh delete mode 100644 apps/dashboard/next.config.mjs delete mode 100644 apps/dashboard/package.json delete mode 100644 apps/dashboard/postcss.config.cjs delete mode 100644 apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/chat/page.tsx delete mode 100644 apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/dashboards/[dashboardId]/list-reports.tsx delete mode 100644 apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/dashboards/[dashboardId]/page.tsx delete mode 100644 apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/dashboards/list-dashboards/header.tsx delete mode 100644 apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/dashboards/list-dashboards/index.tsx delete mode 100644 apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/dashboards/page.tsx delete mode 100644 apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/events/conversions.tsx delete mode 100644 apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/events/events.tsx delete mode 100644 apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/events/page.tsx delete mode 100644 apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/layout-content.tsx delete mode 100644 apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/layout-menu.tsx delete mode 100644 apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/layout-organization-selector.tsx delete mode 100644 apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/layout-sidebar.tsx delete mode 100644 apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/layout-sticky-below-header.tsx delete mode 100644 apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/layout.tsx delete mode 100644 apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/page-layout.tsx delete mode 100644 apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/pages/page.tsx delete mode 100644 apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/pages/pages.tsx delete mode 100644 apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/profiles/[profileId]/most-events/index.tsx delete mode 100644 apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/profiles/[profileId]/page.tsx delete mode 100644 apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/profiles/[profileId]/popular-routes/index.tsx delete mode 100644 apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/profiles/[profileId]/profile-activity/index.tsx delete mode 100644 apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/profiles/[profileId]/profile-activity/profile-activity.tsx delete mode 100644 apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/profiles/[profileId]/profile-charts.tsx delete mode 100644 apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/profiles/[profileId]/profile-events.tsx delete mode 100644 apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/profiles/[profileId]/profile-metrics/index.tsx delete mode 100644 apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/profiles/[profileId]/profile-metrics/profile-metrics.tsx delete mode 100644 apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/profiles/page.tsx delete mode 100644 apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/profiles/power-users.tsx delete mode 100644 apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/profiles/profile-last-seen/index.tsx delete mode 100644 apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/profiles/profiles.tsx delete mode 100644 apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/realtime/map/coordinates.ts delete mode 100644 apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/realtime/map/index.tsx delete mode 100644 apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/realtime/map/map.tsx delete mode 100644 apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/realtime/page.tsx delete mode 100644 apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/realtime/realtime-live-events/index.tsx delete mode 100644 apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/realtime/realtime-live-events/live-events.tsx delete mode 100644 apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/realtime/realtime-reloader.tsx delete mode 100644 apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/reports/[reportId]/page.tsx delete mode 100644 apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/reports/page.tsx delete mode 100644 apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/retention/last-active-users/chart.tsx delete mode 100644 apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/retention/last-active-users/index.tsx delete mode 100644 apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/retention/page.tsx delete mode 100644 apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/retention/rolling-active-users/chart.tsx delete mode 100644 apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/retention/rolling-active-users/index.tsx delete mode 100644 apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/retention/users-retention-series/chart.tsx delete mode 100644 apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/retention/users-retention-series/index.tsx delete mode 100644 apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/retention/weekly-cohorts/index.tsx delete mode 100644 apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/settings/integrations/page.tsx delete mode 100644 apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/settings/loading.tsx delete mode 100644 apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/settings/notifications/page.tsx delete mode 100644 apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/settings/organization/invites/index.tsx delete mode 100644 apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/settings/organization/loading.tsx delete mode 100644 apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/settings/organization/members/index.tsx delete mode 100644 apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/settings/organization/organization/billing.tsx delete mode 100644 apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/settings/organization/organization/usage.tsx delete mode 100644 apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/settings/organization/page.tsx delete mode 100644 apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/settings/page.tsx delete mode 100644 apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/settings/profile/edit-profile.tsx delete mode 100644 apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/settings/profile/loading.tsx delete mode 100644 apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/settings/profile/logout.tsx delete mode 100644 apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/settings/profile/page.tsx delete mode 100644 apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/settings/projects/loading.tsx delete mode 100644 apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/settings/projects/page.tsx delete mode 100644 apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/settings/projects/project-clients.tsx delete mode 100644 apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/settings/references/list-references.tsx delete mode 100644 apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/settings/references/loading.tsx delete mode 100644 apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/settings/references/page.tsx delete mode 100644 apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/side-effects-free-plan.tsx delete mode 100644 apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/side-effects-timezone.tsx delete mode 100644 apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/side-effects-trial.tsx delete mode 100644 apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/side-effects.tsx delete mode 100644 apps/dashboard/src/app/(app)/[organizationSlug]/page.tsx delete mode 100644 apps/dashboard/src/app/(app)/page.tsx delete mode 100644 apps/dashboard/src/app/(auth)/layout.tsx delete mode 100644 apps/dashboard/src/app/(auth)/live-events/index.tsx delete mode 100644 apps/dashboard/src/app/(auth)/login/page.tsx delete mode 100644 apps/dashboard/src/app/(auth)/reset-password/page.tsx delete mode 100644 apps/dashboard/src/app/(onboarding)/layout.tsx delete mode 100644 apps/dashboard/src/app/(onboarding)/onboarding-layout.tsx delete mode 100644 apps/dashboard/src/app/(onboarding)/onboarding/[projectId]/connect/onboarding-connect.tsx delete mode 100644 apps/dashboard/src/app/(onboarding)/onboarding/[projectId]/connect/page.tsx delete mode 100644 apps/dashboard/src/app/(onboarding)/onboarding/[projectId]/verify/onboarding-verify.tsx delete mode 100644 apps/dashboard/src/app/(onboarding)/onboarding/[projectId]/verify/page.tsx delete mode 100644 apps/dashboard/src/app/(onboarding)/onboarding/page.tsx delete mode 100644 apps/dashboard/src/app/(onboarding)/onboarding/project/page.tsx delete mode 100644 apps/dashboard/src/app/(public)/share/overview/[id]/page.tsx delete mode 100644 apps/dashboard/src/app/api/healthcheck/route.tsx delete mode 100644 apps/dashboard/src/app/favicon.ico delete mode 100644 apps/dashboard/src/app/global-error.tsx delete mode 100644 apps/dashboard/src/app/layout.tsx delete mode 100644 apps/dashboard/src/app/maintenance/page.tsx delete mode 100644 apps/dashboard/src/app/manifest.ts delete mode 100644 apps/dashboard/src/app/providers.tsx delete mode 100644 apps/dashboard/src/app/robots.txt delete mode 100644 apps/dashboard/src/components/auth/or.tsx delete mode 100644 apps/dashboard/src/components/auth/reset-password-form.tsx delete mode 100644 apps/dashboard/src/components/clients/client-actions.tsx delete mode 100644 apps/dashboard/src/components/clients/table/columns.tsx delete mode 100644 apps/dashboard/src/components/clients/table/index.tsx delete mode 100644 apps/dashboard/src/components/dark-mode-toggle.tsx delete mode 100644 apps/dashboard/src/components/data-table.tsx delete mode 100644 apps/dashboard/src/components/events/event-field-value.tsx delete mode 100644 apps/dashboard/src/components/events/table/events-data-table.tsx delete mode 100644 apps/dashboard/src/components/events/table/events-table-columns.tsx delete mode 100644 apps/dashboard/src/components/events/table/index.tsx delete mode 100644 apps/dashboard/src/components/links.tsx delete mode 100644 apps/dashboard/src/components/notifications/notification-provider.tsx delete mode 100644 apps/dashboard/src/components/notifications/notifications.tsx delete mode 100644 apps/dashboard/src/components/notifications/table/index.tsx delete mode 100644 apps/dashboard/src/components/overview/live-counter/index.tsx delete mode 100644 apps/dashboard/src/components/overview/live-counter/live-counter.tsx delete mode 100644 apps/dashboard/src/components/overview/overview-hydrate-options.tsx delete mode 100644 apps/dashboard/src/components/overview/overview-live-histogram.tsx delete mode 100644 apps/dashboard/src/components/overview/overview-share/index.tsx delete mode 100644 apps/dashboard/src/components/overview/overview-top-bots.tsx delete mode 100644 apps/dashboard/src/components/overview/overview-top-events/index.tsx delete mode 100644 apps/dashboard/src/components/page-tabs.tsx delete mode 100644 apps/dashboard/src/components/pagination.tsx delete mode 100644 apps/dashboard/src/components/profiles/table/index.tsx delete mode 100644 apps/dashboard/src/components/projects/project-card.tsx delete mode 100644 apps/dashboard/src/components/references/table.tsx delete mode 100644 apps/dashboard/src/components/settings-toggle.tsx delete mode 100644 apps/dashboard/src/components/settings/invites/columns.tsx delete mode 100644 apps/dashboard/src/components/settings/invites/index.tsx delete mode 100644 apps/dashboard/src/components/settings/members/columns.tsx delete mode 100644 apps/dashboard/src/components/settings/members/index.tsx delete mode 100644 apps/dashboard/src/components/sign-out-button.tsx delete mode 100644 apps/dashboard/src/components/ui/badge.tsx delete mode 100644 apps/dashboard/src/components/ui/button.tsx delete mode 100644 apps/dashboard/src/components/ui/calendar.tsx delete mode 100644 apps/dashboard/src/components/ui/command.tsx delete mode 100644 apps/dashboard/src/components/ui/dialog.tsx delete mode 100644 apps/dashboard/src/components/ui/scroll-area.tsx delete mode 100644 apps/dashboard/src/components/ui/table.tsx delete mode 100644 apps/dashboard/src/env.mjs delete mode 100644 apps/dashboard/src/hocs/with-loading-widget.tsx delete mode 100644 apps/dashboard/src/hocs/with-suspense.tsx delete mode 100644 apps/dashboard/src/hooks/useAppParams.ts delete mode 100644 apps/dashboard/src/hooks/useAuth.tsx delete mode 100644 apps/dashboard/src/hooks/useCursor.ts delete mode 100644 apps/dashboard/src/hooks/useEventNames.ts delete mode 100644 apps/dashboard/src/hooks/useEventProperties.ts delete mode 100644 apps/dashboard/src/hooks/useLogout.ts delete mode 100644 apps/dashboard/src/hooks/useMappings.ts delete mode 100644 apps/dashboard/src/hooks/useProfileProperties.ts delete mode 100644 apps/dashboard/src/hooks/useProfileValues.ts delete mode 100644 apps/dashboard/src/hooks/usePropertyValues.ts delete mode 100644 apps/dashboard/src/instrumentation.ts delete mode 100644 apps/dashboard/src/mappings.json delete mode 100644 apps/dashboard/src/middleware.ts delete mode 100644 apps/dashboard/src/modals/SaveReport.tsx delete mode 100644 apps/dashboard/src/modals/Testimonial.tsx delete mode 100644 apps/dashboard/src/modals/index.tsx delete mode 100644 apps/dashboard/src/styles/globals.css delete mode 100644 apps/dashboard/src/trpc/client.tsx delete mode 100644 apps/dashboard/src/utils/casing.ts delete mode 100644 apps/dashboard/src/utils/getters.ts delete mode 100644 apps/dashboard/src/utils/meta.ts delete mode 100644 apps/dashboard/src/utils/theme.ts delete mode 100644 apps/dashboard/tailwind.config.js delete mode 100644 apps/dashboard/tsconfig.json delete mode 100644 apps/docs/Dockerfile delete mode 100644 apps/docs/index.js delete mode 100644 apps/docs/package.json create mode 100644 apps/start/.cta.json create mode 100644 apps/start/.cursorrules create mode 100644 apps/start/.gitignore create mode 100644 apps/start/.vscode/settings.json create mode 100644 apps/start/README.md create mode 100644 apps/start/ROUTING.md create mode 100644 apps/start/biome.json create mode 100644 apps/start/components.json create mode 100644 apps/start/package.json rename apps/{dashboard => start}/public/favicon.ico (100%) create mode 100644 apps/start/public/img-1.png create mode 100644 apps/start/public/img-2.png create mode 100644 apps/start/public/img-3.png create mode 100644 apps/start/public/img-4.png create mode 100644 apps/start/public/img-5.png create mode 100644 apps/start/public/img-6.png rename apps/{dashboard => start}/public/logo.svg (100%) create mode 100644 apps/start/public/logo192.png create mode 100644 apps/start/public/logo512.png create mode 100644 apps/start/public/manifest.json create mode 100644 apps/start/public/robots.txt create mode 100644 apps/start/src/app/global-middleware.ts rename apps/{dashboard => start}/src/components/animate-height.tsx (100%) create mode 100644 apps/start/src/components/animated-number.tsx create mode 100644 apps/start/src/components/auth/or.tsx create mode 100644 apps/start/src/components/auth/reset-password-form.tsx rename apps/{dashboard => start}/src/components/auth/share-enter-password.tsx (82%) rename apps/{dashboard => start}/src/components/auth/sign-in-email-form.tsx (56%) rename apps/{dashboard => start}/src/components/auth/sign-in-github.tsx (69%) rename apps/{dashboard => start}/src/components/auth/sign-in-google.tsx (68%) rename apps/{dashboard => start}/src/components/auth/sign-up-email-form.tsx (68%) rename apps/{dashboard => start}/src/components/button-container.tsx (100%) rename apps/{dashboard => start}/src/components/card.tsx (98%) rename apps/{dashboard => start}/src/components/chart-ssr.tsx (79%) rename apps/{dashboard => start}/src/components/charts/chart-tooltip.tsx (100%) create mode 100644 apps/start/src/components/charts/common-bar.tsx rename apps/{dashboard => start}/src/components/chat/chat-form.tsx (100%) rename apps/{dashboard => start}/src/components/chat/chat-message.tsx (100%) rename apps/{dashboard => start}/src/components/chat/chat-messages.tsx (92%) rename apps/{dashboard => start}/src/components/chat/chat-report.tsx (99%) rename apps/{dashboard => start}/src/components/chat/chat.tsx (88%) rename apps/{dashboard => start}/src/components/click-to-copy.tsx (97%) rename apps/{dashboard => start}/src/components/clients/create-client-success.tsx (95%) create mode 100644 apps/start/src/components/clients/table/columns.tsx create mode 100644 apps/start/src/components/clients/table/index.tsx rename apps/{dashboard => start}/src/components/color-square.tsx (100%) rename apps/{dashboard => start}/src/components/dot.tsx (100%) rename apps/{dashboard => start}/src/components/events/event-icon.tsx (100%) rename apps/{dashboard => start}/src/components/events/event-list-item.tsx (76%) rename apps/{dashboard => start}/src/components/events/event-listener.tsx (53%) rename apps/{dashboard => start}/src/components/events/list-properties-icon.tsx (100%) rename apps/{dashboard => start}/src/components/events/table/columns.tsx (89%) create mode 100644 apps/start/src/components/events/table/index.tsx create mode 100644 apps/start/src/components/events/table/item.tsx rename apps/{dashboard => start}/src/components/fade-in.tsx (97%) create mode 100644 apps/start/src/components/feedback-button.tsx rename apps/{dashboard => start}/src/components/forms/checkbox-item.tsx (100%) rename apps/{dashboard => start}/src/components/forms/copy-input.tsx (93%) rename apps/{dashboard => start}/src/components/forms/input-with-label.tsx (100%) rename apps/{dashboard => start}/src/components/forms/tag-input.tsx (99%) rename apps/{dashboard => start}/src/components/full-page-empty-state.tsx (83%) create mode 100644 apps/start/src/components/full-page-error-state.tsx rename apps/{dashboard => start}/src/components/full-page-loading-state.tsx (68%) rename apps/{dashboard => start}/src/components/full-width-navbar.tsx (89%) rename apps/{dashboard => start}/src/components/fullscreen-toggle.tsx (95%) rename apps/{dashboard => start}/src/components/grid-table.tsx (100%) rename apps/{dashboard => start}/src/components/integrations/active-integrations.tsx (85%) rename apps/{dashboard => start}/src/components/integrations/all-integrations.tsx (93%) rename apps/{dashboard => start}/src/components/integrations/forms/discord-integration.tsx (84%) rename apps/{dashboard => start}/src/components/integrations/forms/slack-integration.tsx (71%) rename apps/{dashboard => start}/src/components/integrations/forms/webhook-integration.tsx (79%) rename apps/{dashboard => start}/src/components/integrations/integration-card.tsx (100%) rename apps/{dashboard => start}/src/components/integrations/integrations.tsx (100%) create mode 100644 apps/start/src/components/lazy-component.tsx create mode 100644 apps/start/src/components/links.tsx create mode 100644 apps/start/src/components/login-left-panel.tsx rename apps/{dashboard => start}/src/components/logo.tsx (100%) rename apps/{dashboard => start}/src/components/markdown.tsx (100%) rename apps/{dashboard/src/app/(auth)/live-events/live-events.tsx => start/src/components/mock-event-list.tsx} (96%) create mode 100644 apps/start/src/components/notifications/notification-provider.tsx rename apps/{dashboard => start}/src/components/notifications/notification-rules.tsx (81%) create mode 100644 apps/start/src/components/notifications/notifications.tsx rename apps/{dashboard => start}/src/components/notifications/rule-card.tsx (87%) rename apps/{dashboard => start}/src/components/notifications/table/columns.tsx (77%) create mode 100644 apps/start/src/components/notifications/table/index.tsx create mode 100644 apps/start/src/components/onboarding-left-panel.tsx rename apps/{dashboard/src/app/(onboarding)/onboarding/[projectId]/connect => start/src/components/onboarding}/connect-app.tsx (87%) rename apps/{dashboard/src/app/(onboarding)/onboarding/[projectId]/connect => start/src/components/onboarding}/connect-backend.tsx (81%) rename apps/{dashboard/src/app/(onboarding)/onboarding/[projectId]/connect => start/src/components/onboarding}/connect-web.tsx (89%) create mode 100644 apps/start/src/components/onboarding/curl-preview.tsx rename apps/{dashboard/src/app/(onboarding)/onboarding/[projectId]/verify => start/src/components/onboarding}/onboarding-verify-listener.tsx (76%) rename apps/{dashboard/src/app/(onboarding) => start/src/components/onboarding}/skip-onboarding.tsx (58%) rename apps/{dashboard/src/app/(onboarding) => start/src/components/onboarding}/steps.tsx (93%) rename apps/{dashboard/src/app/(app)/[organizationSlug]/[projectId]/settings/organization => start/src/components}/organization/billing-faq.tsx (96%) create mode 100644 apps/start/src/components/organization/billing.tsx rename apps/{dashboard/src/app/(app)/[organizationSlug]/[projectId]/settings/organization => start/src/components}/organization/current-subscription.tsx (81%) rename apps/{dashboard/src/app/(app)/[organizationSlug]/[projectId]/settings/organization => start/src/components}/organization/edit-organization.tsx (79%) rename apps/{dashboard/src/app/(app)/[organizationSlug]/[projectId]/settings/organization => start/src/components}/organization/organization.tsx (100%) create mode 100644 apps/start/src/components/organization/usage.tsx rename apps/{dashboard => start}/src/components/overview/filters/origin-filter.tsx (67%) rename apps/{dashboard => start}/src/components/overview/filters/overview-filters-buttons.tsx (94%) rename apps/{dashboard => start}/src/components/overview/filters/overview-filters-drawer-content.tsx (91%) rename apps/{dashboard => start}/src/components/overview/filters/overview-filters-drawer.tsx (98%) create mode 100644 apps/start/src/components/overview/live-counter.tsx rename apps/{dashboard => start}/src/components/overview/overview-chart-toggle.tsx (100%) rename apps/{dashboard => start}/src/components/overview/overview-constants.tsx (100%) rename apps/{dashboard => start}/src/components/overview/overview-details-button.tsx (100%) rename apps/{dashboard => start}/src/components/overview/overview-interval.tsx (89%) create mode 100644 apps/start/src/components/overview/overview-live-histogram.tsx rename apps/{dashboard => start}/src/components/overview/overview-metric-card.tsx (90%) rename apps/{dashboard => start}/src/components/overview/overview-metrics.tsx (53%) rename apps/{dashboard => start}/src/components/overview/overview-range.tsx (97%) rename apps/{dashboard/src/components/overview/overview-share => start/src/components/overview}/overview-share.tsx (67%) rename apps/{dashboard => start}/src/components/overview/overview-top-devices.tsx (94%) rename apps/{dashboard/src/components/overview/overview-top-events => start/src/components/overview}/overview-top-events.tsx (84%) rename apps/{dashboard => start}/src/components/overview/overview-top-generic-modal.tsx (78%) rename apps/{dashboard => start}/src/components/overview/overview-top-geo.tsx (89%) rename apps/{dashboard => start}/src/components/overview/overview-top-pages-modal.tsx (65%) rename apps/{dashboard => start}/src/components/overview/overview-top-pages.tsx (86%) rename apps/{dashboard => start}/src/components/overview/overview-top-sources.tsx (89%) rename apps/{dashboard => start}/src/components/overview/overview-widget-table.tsx (98%) rename apps/{dashboard => start}/src/components/overview/overview-widget.tsx (79%) rename apps/{dashboard => start}/src/components/overview/useOverviewOptions.ts (94%) rename apps/{dashboard => start}/src/components/overview/useOverviewWidget.tsx (100%) create mode 100644 apps/start/src/components/page-container.tsx create mode 100644 apps/start/src/components/page-header.tsx create mode 100644 apps/start/src/components/pagination-floating.tsx create mode 100644 apps/start/src/components/pagination.tsx rename apps/{dashboard => start}/src/components/ping.tsx (100%) create mode 100644 apps/start/src/components/profile-toggle.tsx create mode 100644 apps/start/src/components/profiles/latest-events.tsx rename apps/{dashboard/src/app/(app)/[organizationSlug]/[projectId]/profiles/[profileId]/most-events => start/src/components/profiles}/most-events.tsx (75%) rename apps/{dashboard/src/app/(app)/[organizationSlug]/[projectId]/profiles/[profileId]/popular-routes => start/src/components/profiles}/popular-routes.tsx (73%) create mode 100644 apps/start/src/components/profiles/profile-activity.tsx rename apps/{dashboard => start}/src/components/profiles/profile-avatar.tsx (75%) create mode 100644 apps/start/src/components/profiles/profile-charts.tsx create mode 100644 apps/start/src/components/profiles/profile-metrics.tsx create mode 100644 apps/start/src/components/profiles/profile-properties.tsx rename apps/{dashboard => start}/src/components/profiles/table/columns.tsx (87%) create mode 100644 apps/start/src/components/profiles/table/index.tsx rename apps/{dashboard/src/app/(app)/[organizationSlug]/[projectId]/layout-project-selector.tsx => start/src/components/project-selector.tsx} (74%) create mode 100644 apps/start/src/components/projects/project-card.tsx create mode 100644 apps/start/src/components/providers.tsx rename apps/{dashboard => start}/src/components/react-virtualized-auto-sizer.tsx (100%) create mode 100644 apps/start/src/components/realtime/map/coordinates.ts create mode 100644 apps/start/src/components/realtime/map/index.tsx rename apps/{dashboard/src/app/(app)/[organizationSlug]/[projectId] => start/src/components}/realtime/map/map.helpers.tsx (98%) rename apps/{dashboard/src/app/(app)/[organizationSlug]/[projectId] => start/src/components}/realtime/map/markers.ts (62%) create mode 100644 apps/start/src/components/realtime/realtime-active-sessions.tsx create mode 100644 apps/start/src/components/realtime/realtime-geo.tsx rename apps/{dashboard/src/app/(app)/[organizationSlug]/[projectId] => start/src/components}/realtime/realtime-live-histogram.tsx (78%) create mode 100644 apps/start/src/components/realtime/realtime-paths.tsx create mode 100644 apps/start/src/components/realtime/realtime-referrals.tsx create mode 100644 apps/start/src/components/realtime/realtime-reloader.tsx rename apps/{dashboard => start}/src/components/report-chart/area/chart.tsx (92%) rename apps/{dashboard => start}/src/components/report-chart/area/index.tsx (72%) rename apps/{dashboard => start}/src/components/report-chart/aspect-container.tsx (97%) rename apps/{dashboard => start}/src/components/report-chart/bar/chart.tsx (53%) rename apps/{dashboard => start}/src/components/report-chart/bar/index.tsx (78%) rename apps/{dashboard => start}/src/components/report-chart/common/axis.tsx (83%) rename apps/{dashboard => start}/src/components/report-chart/common/empty.tsx (100%) rename apps/{dashboard => start}/src/components/report-chart/common/error.tsx (100%) rename apps/{dashboard => start}/src/components/report-chart/common/linear-gradient.tsx (100%) rename apps/{dashboard => start}/src/components/report-chart/common/loading.tsx (100%) rename apps/{dashboard => start}/src/components/report-chart/common/previous-diff-indicator.tsx (94%) rename apps/{dashboard => start}/src/components/report-chart/common/report-chart-tooltip.tsx (95%) rename apps/{dashboard => start}/src/components/report-chart/common/report-table.tsx (97%) rename apps/{dashboard => start}/src/components/report-chart/common/serie-icon.flags.tsx (100%) rename apps/{dashboard => start}/src/components/report-chart/common/serie-icon.tsx (82%) rename apps/{dashboard => start}/src/components/report-chart/common/serie-icon.urls.ts (89%) rename apps/{dashboard => start}/src/components/report-chart/common/serie-name.tsx (100%) rename apps/{dashboard => start}/src/components/report-chart/context.tsx (100%) rename apps/{dashboard => start}/src/components/report-chart/conversion/chart.tsx (92%) rename apps/{dashboard => start}/src/components/report-chart/conversion/index.tsx (75%) rename apps/{dashboard => start}/src/components/report-chart/conversion/summary.tsx (99%) rename apps/{dashboard => start}/src/components/report-chart/funnel/chart.tsx (98%) rename apps/{dashboard => start}/src/components/report-chart/funnel/index.tsx (83%) rename apps/{dashboard => start}/src/components/report-chart/histogram/chart.tsx (70%) rename apps/{dashboard => start}/src/components/report-chart/histogram/index.tsx (73%) rename apps/{dashboard => start}/src/components/report-chart/index.tsx (90%) rename apps/{dashboard => start}/src/components/report-chart/line/chart.tsx (69%) rename apps/{dashboard => start}/src/components/report-chart/line/index.tsx (73%) rename apps/{dashboard => start}/src/components/report-chart/map/chart.tsx (78%) rename apps/{dashboard => start}/src/components/report-chart/map/index.tsx (71%) rename apps/{dashboard => start}/src/components/report-chart/metric/chart.tsx (91%) rename apps/{dashboard => start}/src/components/report-chart/metric/index.tsx (79%) rename apps/{dashboard => start}/src/components/report-chart/metric/metric-card.tsx (97%) rename apps/{dashboard => start}/src/components/report-chart/pie/chart.tsx (98%) rename apps/{dashboard => start}/src/components/report-chart/pie/index.tsx (72%) rename apps/{dashboard/src/app/(app)/[organizationSlug]/[projectId]/reports => start/src/components/report-chart}/report-editor.tsx (92%) rename apps/{dashboard => start}/src/components/report-chart/retention/chart.tsx (99%) rename apps/{dashboard => start}/src/components/report-chart/retention/index.tsx (76%) rename apps/{dashboard => start}/src/components/report-chart/retention/table.tsx (98%) rename apps/{dashboard => start}/src/components/report-chart/retention/tooltip.tsx (95%) rename apps/{dashboard => start}/src/components/report-chart/shortcut.tsx (100%) rename apps/{dashboard => start}/src/components/report/ReportChartType.tsx (100%) rename apps/{dashboard => start}/src/components/report/ReportInterval.tsx (100%) rename apps/{dashboard => start}/src/components/report/ReportLineType.tsx (100%) rename apps/{dashboard => start}/src/components/report/ReportSaveButton.tsx (58%) rename apps/{dashboard => start}/src/components/report/ReportSegment.tsx (100%) rename apps/{dashboard => start}/src/components/report/edit-report-name.tsx (99%) rename apps/{dashboard => start}/src/components/report/reportSlice.ts (100%) rename apps/{dashboard => start}/src/components/report/sidebar/EventPropertiesCombobox.tsx (92%) rename apps/{dashboard => start}/src/components/report/sidebar/PropertiesCombobox.tsx (98%) rename apps/{dashboard => start}/src/components/report/sidebar/ReportBreakdownMore.tsx (100%) rename apps/{dashboard => start}/src/components/report/sidebar/ReportBreakdowns.tsx (96%) rename apps/{dashboard => start}/src/components/report/sidebar/ReportEventMore.tsx (100%) rename apps/{dashboard => start}/src/components/report/sidebar/ReportEvents.tsx (98%) rename apps/{dashboard => start}/src/components/report/sidebar/ReportFormula.tsx (97%) rename apps/{dashboard => start}/src/components/report/sidebar/ReportSettings.tsx (99%) rename apps/{dashboard => start}/src/components/report/sidebar/ReportSidebar.tsx (100%) rename apps/{dashboard => start}/src/components/report/sidebar/filters/FilterItem.tsx (92%) rename apps/{dashboard => start}/src/components/report/sidebar/filters/FiltersList.tsx (100%) create mode 100644 apps/start/src/components/selling-points.tsx create mode 100644 apps/start/src/components/sessions/table/columns.tsx create mode 100644 apps/start/src/components/sessions/table/index.tsx rename apps/{dashboard/src/app/(app)/[organizationSlug]/[projectId]/settings/projects => start/src/components/settings}/delete-project.tsx (72%) rename apps/{dashboard/src/app/(app)/[organizationSlug]/[projectId]/settings/projects => start/src/components/settings}/edit-project-details.tsx (90%) rename apps/{dashboard/src/app/(app)/[organizationSlug]/[projectId]/settings/projects => start/src/components/settings}/edit-project-filters.tsx (84%) create mode 100644 apps/start/src/components/settings/invites/columns.tsx create mode 100644 apps/start/src/components/settings/invites/index.tsx create mode 100644 apps/start/src/components/settings/members/columns.tsx create mode 100644 apps/start/src/components/settings/members/index.tsx create mode 100644 apps/start/src/components/sidebar-link.tsx create mode 100644 apps/start/src/components/sidebar-organization-menu.tsx create mode 100644 apps/start/src/components/sidebar-project-menu.tsx create mode 100644 apps/start/src/components/sidebar.tsx create mode 100644 apps/start/src/components/skeleton-dashboard.tsx rename apps/{dashboard => start}/src/components/skeleton.tsx (100%) rename apps/{dashboard => start}/src/components/stats.tsx (100%) rename apps/{dashboard => start}/src/components/syntax.tsx (99%) create mode 100644 apps/start/src/components/theme-provider.tsx rename apps/{dashboard => start}/src/components/time-window-picker.tsx (100%) rename apps/{dashboard => start}/src/components/tooltip-complete.tsx (100%) rename apps/{dashboard => start}/src/components/ui/RenderDots.tsx (99%) rename apps/{dashboard => start}/src/components/ui/accordion.tsx (100%) rename apps/{dashboard => start}/src/components/ui/alert-dialog.tsx (99%) rename apps/{dashboard => start}/src/components/ui/alert.tsx (100%) rename apps/{dashboard => start}/src/components/ui/aspect-ratio.tsx (90%) rename apps/{dashboard => start}/src/components/ui/avatar.tsx (98%) create mode 100644 apps/start/src/components/ui/badge.tsx create mode 100644 apps/start/src/components/ui/button.tsx create mode 100644 apps/start/src/components/ui/calendar.tsx rename apps/{dashboard => start}/src/components/ui/carousel.tsx (94%) rename apps/{dashboard => start}/src/components/ui/checkbox.tsx (99%) rename apps/{dashboard => start}/src/components/ui/combobox-advanced.tsx (80%) rename apps/{dashboard => start}/src/components/ui/combobox-events.tsx (95%) rename apps/{dashboard => start}/src/components/ui/combobox.tsx (99%) create mode 100644 apps/start/src/components/ui/command.tsx create mode 100644 apps/start/src/components/ui/data-table/data-table-column-header.tsx create mode 100644 apps/start/src/components/ui/data-table/data-table-config.ts create mode 100644 apps/start/src/components/ui/data-table/data-table-date-filter.tsx create mode 100644 apps/start/src/components/ui/data-table/data-table-faceted-filter.tsx create mode 100644 apps/start/src/components/ui/data-table/data-table-helpers.tsx create mode 100644 apps/start/src/components/ui/data-table/data-table-hooks.tsx create mode 100644 apps/start/src/components/ui/data-table/data-table-parsers.ts create mode 100644 apps/start/src/components/ui/data-table/data-table-slider-filter.tsx create mode 100644 apps/start/src/components/ui/data-table/data-table-toolbar.tsx create mode 100644 apps/start/src/components/ui/data-table/data-table-view-options.tsx create mode 100644 apps/start/src/components/ui/data-table/data-table.tsx create mode 100644 apps/start/src/components/ui/data-table/use-table.tsx create mode 100644 apps/start/src/components/ui/data-table/virtualized-data-table.tsx create mode 100644 apps/start/src/components/ui/date-time.tsx create mode 100644 apps/start/src/components/ui/dialog.tsx rename apps/{dashboard => start}/src/components/ui/dropdown-menu.tsx (95%) rename apps/{dashboard => start}/src/components/ui/gradient-background.tsx (100%) create mode 100644 apps/start/src/components/ui/input-date-time.tsx rename apps/{dashboard => start}/src/components/ui/input-enter.tsx (97%) rename apps/{dashboard => start}/src/components/ui/input-otp.tsx (100%) rename apps/{dashboard => start}/src/components/ui/input-with-toggle.tsx (100%) rename apps/{dashboard => start}/src/components/ui/input.tsx (94%) create mode 100644 apps/start/src/components/ui/key-value-grid.tsx rename apps/{dashboard => start}/src/components/ui/key-value.tsx (95%) rename apps/{dashboard => start}/src/components/ui/label.tsx (85%) rename apps/{dashboard => start}/src/components/ui/padding.tsx (100%) rename apps/{dashboard => start}/src/components/ui/popover.tsx (92%) rename apps/{dashboard => start}/src/components/ui/progress.tsx (100%) rename apps/{dashboard => start}/src/components/ui/radio-group.tsx (100%) create mode 100644 apps/start/src/components/ui/scroll-area.tsx create mode 100644 apps/start/src/components/ui/select.tsx create mode 100644 apps/start/src/components/ui/separator.tsx rename apps/{dashboard => start}/src/components/ui/sheet.tsx (99%) create mode 100644 apps/start/src/components/ui/slider.tsx rename apps/{dashboard => start}/src/components/ui/sonner.tsx (94%) create mode 100644 apps/start/src/components/ui/spinner.tsx rename apps/{dashboard => start}/src/components/ui/switch.tsx (98%) create mode 100644 apps/start/src/components/ui/table.tsx rename apps/{dashboard => start}/src/components/ui/tabs.tsx (67%) rename apps/{dashboard => start}/src/components/ui/textarea.tsx (100%) rename apps/{dashboard => start}/src/components/ui/toast.tsx (99%) rename apps/{dashboard => start}/src/components/ui/toaster.tsx (93%) rename apps/{dashboard => start}/src/components/ui/toggle-group.tsx (100%) rename apps/{dashboard => start}/src/components/ui/toggle.tsx (100%) rename apps/{dashboard => start}/src/components/ui/tooltip.tsx (99%) rename apps/{dashboard => start}/src/components/widget-table.tsx (100%) rename apps/{dashboard => start}/src/components/widget.tsx (100%) create mode 100644 apps/start/src/hooks/use-app-context.ts create mode 100644 apps/start/src/hooks/use-app-params.ts rename apps/{dashboard/src/hooks/useBreakpoint.ts => start/src/hooks/use-breakpoint.ts} (77%) create mode 100644 apps/start/src/hooks/use-callback-ref.ts rename apps/{dashboard/src/hooks/useClientSecret.ts => start/src/hooks/use-client-secret.ts} (100%) rename apps/{dashboard => start}/src/hooks/use-dashed-stroke.tsx (100%) rename apps/{dashboard/src/hooks/useDebounceFn.ts => start/src/hooks/use-debounce-fn.ts} (76%) rename apps/{dashboard/src/hooks/useDebounceState.ts => start/src/hooks/use-debounce-state.ts} (85%) rename apps/{dashboard/src/hooks/useDebounceValue.ts => start/src/hooks/use-debounce-value.ts} (100%) create mode 100644 apps/start/src/hooks/use-debounced-callback.ts create mode 100644 apps/start/src/hooks/use-event-names.ts create mode 100644 apps/start/src/hooks/use-event-properties.ts rename apps/{dashboard/src/hooks/useEventQueryFilters.ts => start/src/hooks/use-event-query-filters.ts} (100%) rename apps/{dashboard/src/hooks/useFormatDateInterval.ts => start/src/hooks/use-format-date-interval.ts} (100%) create mode 100644 apps/start/src/hooks/use-logout.ts rename apps/{dashboard/src/hooks/useNumerFormatter.ts => start/src/hooks/use-numer-formatter.ts} (92%) create mode 100644 apps/start/src/hooks/use-page-tabs.ts create mode 100644 apps/start/src/hooks/use-profile-properties.ts create mode 100644 apps/start/src/hooks/use-profile-values.ts create mode 100644 apps/start/src/hooks/use-property-values.ts rename apps/{dashboard/src/hooks/useRechartDataModel.ts => start/src/hooks/use-rechart-data-model.ts} (99%) rename apps/{dashboard => start}/src/hooks/use-scroll-anchor.ts (100%) create mode 100644 apps/start/src/hooks/use-search-query-state.ts create mode 100644 apps/start/src/hooks/use-session-extension.ts create mode 100644 apps/start/src/hooks/use-theme.ts rename apps/{dashboard/src/hooks/useThrottle.ts => start/src/hooks/use-throttle.ts} (100%) rename apps/{dashboard/src/hooks/useVisibleSeries.ts => start/src/hooks/use-visible-series.ts} (98%) rename apps/{dashboard/src/hooks/useWS.ts => start/src/hooks/use-ws.ts} (75%) create mode 100644 apps/start/src/integrations/tanstack-query/devtools.tsx create mode 100644 apps/start/src/integrations/tanstack-query/root-provider.tsx create mode 100644 apps/start/src/integrations/trpc/react.ts create mode 100644 apps/start/src/lib/utils.ts create mode 100644 apps/start/src/logo.svg rename apps/{dashboard => start}/src/modals/Instructions.tsx (100%) rename apps/{dashboard => start}/src/modals/Modal/Container.tsx (82%) rename apps/{dashboard/src/modals/AddClient.tsx => start/src/modals/add-client.tsx} (87%) rename apps/{dashboard/src/modals/AddDashboard.tsx => start/src/modals/add-dashboard.tsx} (63%) rename apps/{dashboard => start}/src/modals/add-integration.tsx (82%) rename apps/{dashboard => start}/src/modals/add-notification-rule.tsx (91%) rename apps/{dashboard => start}/src/modals/add-project.tsx (88%) rename apps/{dashboard/src/modals/AddReference.tsx => start/src/modals/add-reference.tsx} (58%) rename apps/{dashboard/src/modals/Confirm.tsx => start/src/modals/confirm.tsx} (100%) rename apps/{dashboard/src/app/(app)/[organizationSlug]/[projectId]/settings/organization/invites => start/src/modals}/create-invite.tsx (83%) rename apps/{dashboard/src/modals/DateRangerPicker.tsx => start/src/modals/date-ranger-picker.tsx} (51%) create mode 100644 apps/start/src/modals/date-time-picker.tsx rename apps/{dashboard/src/modals/EditClient.tsx => start/src/modals/edit-client.tsx} (69%) rename apps/{dashboard/src/modals/EditDashboard.tsx => start/src/modals/edit-dashboard.tsx} (72%) rename apps/{dashboard => start}/src/modals/edit-event.tsx (80%) create mode 100644 apps/start/src/modals/edit-member.tsx create mode 100644 apps/start/src/modals/edit-reference.tsx rename apps/{dashboard/src/modals/EditReport.tsx => start/src/modals/edit-report.tsx} (100%) rename apps/{dashboard => start}/src/modals/event-details.tsx (55%) create mode 100644 apps/start/src/modals/index.tsx rename apps/{dashboard/src/modals/OnboardingTroubleshoot.tsx => start/src/modals/onboarding-troubleshoot.tsx} (100%) rename apps/{dashboard/src/modals/OverviewChartDetails.tsx => start/src/modals/overview-chart-details.tsx} (100%) rename apps/{dashboard => start}/src/modals/request-reset-password.tsx (77%) create mode 100644 apps/start/src/modals/save-report.tsx rename apps/{dashboard/src/modals/ShareOverviewModal.tsx => start/src/modals/share-overview-modal.tsx} (64%) rename apps/{dashboard => start}/src/redux/index.ts (100%) create mode 100644 apps/start/src/routeTree.gen.ts create mode 100644 apps/start/src/router.tsx create mode 100644 apps/start/src/routes/__root.tsx rename apps/{dashboard/src/app/(app)/[organizationSlug]/[projectId]/page.tsx => start/src/routes/_app.$organizationId.$projectId.tsx} (69%) create mode 100644 apps/start/src/routes/_app.$organizationId.$projectId_.chat.tsx rename apps/{dashboard/src/app/(app)/[organizationSlug]/[projectId]/dashboards/list-dashboards/list-dashboards.tsx => start/src/routes/_app.$organizationId.$projectId_.dashboards.tsx} (67%) create mode 100644 apps/start/src/routes/_app.$organizationId.$projectId_.dashboards_.$dashboardId.tsx create mode 100644 apps/start/src/routes/_app.$organizationId.$projectId_.events._tabs.conversions.tsx create mode 100644 apps/start/src/routes/_app.$organizationId.$projectId_.events._tabs.events.tsx create mode 100644 apps/start/src/routes/_app.$organizationId.$projectId_.events._tabs.index.tsx rename apps/{dashboard/src/app/(app)/[organizationSlug]/[projectId]/events/charts.tsx => start/src/routes/_app.$organizationId.$projectId_.events._tabs.stats.tsx} (94%) create mode 100644 apps/start/src/routes/_app.$organizationId.$projectId_.events._tabs.tsx create mode 100644 apps/start/src/routes/_app.$organizationId.$projectId_.notifications._tabs.index.tsx create mode 100644 apps/start/src/routes/_app.$organizationId.$projectId_.notifications._tabs.notifications.tsx create mode 100644 apps/start/src/routes/_app.$organizationId.$projectId_.notifications._tabs.rules.tsx create mode 100644 apps/start/src/routes/_app.$organizationId.$projectId_.notifications._tabs.tsx create mode 100644 apps/start/src/routes/_app.$organizationId.$projectId_.pages.tsx create mode 100644 apps/start/src/routes/_app.$organizationId.$projectId_.profiles.$profileId._tabs.events.tsx create mode 100644 apps/start/src/routes/_app.$organizationId.$projectId_.profiles.$profileId._tabs.index.tsx create mode 100644 apps/start/src/routes/_app.$organizationId.$projectId_.profiles.$profileId._tabs.tsx create mode 100644 apps/start/src/routes/_app.$organizationId.$projectId_.profiles._tabs.anonymous.tsx create mode 100644 apps/start/src/routes/_app.$organizationId.$projectId_.profiles._tabs.identified.tsx create mode 100644 apps/start/src/routes/_app.$organizationId.$projectId_.profiles._tabs.index.tsx create mode 100644 apps/start/src/routes/_app.$organizationId.$projectId_.profiles._tabs.power-users.tsx create mode 100644 apps/start/src/routes/_app.$organizationId.$projectId_.profiles._tabs.tsx create mode 100644 apps/start/src/routes/_app.$organizationId.$projectId_.realtime.tsx create mode 100644 apps/start/src/routes/_app.$organizationId.$projectId_.references.tsx create mode 100644 apps/start/src/routes/_app.$organizationId.$projectId_.reports.tsx create mode 100644 apps/start/src/routes/_app.$organizationId.$projectId_.reports_.$reportId.tsx create mode 100644 apps/start/src/routes/_app.$organizationId.$projectId_.sessions.tsx create mode 100644 apps/start/src/routes/_app.$organizationId.$projectId_.sessions_.$sessionId.tsx create mode 100644 apps/start/src/routes/_app.$organizationId.$projectId_.settings._tabs.clients.tsx create mode 100644 apps/start/src/routes/_app.$organizationId.$projectId_.settings._tabs.details.tsx create mode 100644 apps/start/src/routes/_app.$organizationId.$projectId_.settings._tabs.events.tsx create mode 100644 apps/start/src/routes/_app.$organizationId.$projectId_.settings._tabs.index.tsx create mode 100644 apps/start/src/routes/_app.$organizationId.$projectId_.settings._tabs.tsx create mode 100644 apps/start/src/routes/_app.$organizationId.billing.tsx create mode 100644 apps/start/src/routes/_app.$organizationId.index.tsx create mode 100644 apps/start/src/routes/_app.$organizationId.integrations._tabs.available.tsx create mode 100644 apps/start/src/routes/_app.$organizationId.integrations._tabs.index.tsx create mode 100644 apps/start/src/routes/_app.$organizationId.integrations._tabs.installed.tsx create mode 100644 apps/start/src/routes/_app.$organizationId.integrations._tabs.tsx create mode 100644 apps/start/src/routes/_app.$organizationId.members._tabs.index.tsx create mode 100644 apps/start/src/routes/_app.$organizationId.members._tabs.invitations.tsx create mode 100644 apps/start/src/routes/_app.$organizationId.members._tabs.members.tsx create mode 100644 apps/start/src/routes/_app.$organizationId.members._tabs.tsx create mode 100644 apps/start/src/routes/_app.$organizationId.settings.tsx create mode 100644 apps/start/src/routes/_app.$organizationId.tsx create mode 100644 apps/start/src/routes/_app.tsx create mode 100644 apps/start/src/routes/_login.login.tsx create mode 100644 apps/start/src/routes/_login.reset-password.tsx create mode 100644 apps/start/src/routes/_login.tsx create mode 100644 apps/start/src/routes/_public.onboarding.tsx create mode 100644 apps/start/src/routes/_public.tsx create mode 100644 apps/start/src/routes/_steps.onboarding.$projectId.connect.tsx create mode 100644 apps/start/src/routes/_steps.onboarding.$projectId.verify.tsx rename apps/{dashboard/src/app/(onboarding)/onboarding/project/onboarding-create-project.tsx => start/src/routes/_steps.onboarding.project.tsx} (78%) create mode 100644 apps/start/src/routes/_steps.tsx create mode 100644 apps/start/src/routes/index.tsx create mode 100644 apps/start/src/routes/share.overview.$shareId.tsx create mode 100644 apps/start/src/server/get-envs.ts create mode 100644 apps/start/src/styles.css rename apps/{dashboard => start}/src/translations/countries.ts (100%) rename apps/{dashboard => start}/src/translations/properties.ts (100%) create mode 100644 apps/start/src/trpc/client.ts create mode 100644 apps/start/src/types/data-table.ts rename apps/{dashboard => start}/src/types/index.ts (100%) rename apps/{dashboard => start}/src/types/react-simple-map.d.ts (100%) create mode 100644 apps/start/src/utils/are-props-equal.ts create mode 100644 apps/start/src/utils/casing.ts rename apps/{dashboard => start}/src/utils/clipboard.ts (100%) rename apps/{dashboard => start}/src/utils/cn.ts (100%) rename apps/{dashboard => start}/src/utils/date.ts (62%) rename apps/{dashboard => start}/src/utils/getDbId.ts (100%) create mode 100644 apps/start/src/utils/getters.ts rename apps/{dashboard => start}/src/utils/math.ts (100%) create mode 100644 apps/start/src/utils/op.ts rename apps/{dashboard => start}/src/utils/should-ignore-keypress.ts (60%) rename apps/{dashboard => start}/src/utils/slug.ts (100%) rename apps/{dashboard => start}/src/utils/storage.ts (100%) create mode 100644 apps/start/src/utils/theme.ts create mode 100644 apps/start/src/utils/title.ts rename apps/{dashboard => start}/src/utils/truncate.ts (100%) create mode 100644 apps/start/tsconfig.json create mode 100644 apps/start/vite.config.ts create mode 100644 apps/start/wrangler.jsonc create mode 100644 apps/worker/tsdown.config.ts delete mode 100644 apps/worker/tsup.config.ts create mode 100644 packages/db/prisma/migrations/20251013121758_report_layout/migration.sql create mode 100644 packages/db/prisma/prisma-json-types.ts create mode 100644 packages/db/src/generated/empty create mode 100644 packages/trpc/src/routers/chat.ts create mode 100644 packages/trpc/src/routers/realtime.ts create mode 100644 packages/trpc/src/routers/session.ts delete mode 100644 packages/trpc/src/routers/ticket.ts create mode 100644 patches/nuqs.patch create mode 100644 tooling/unused-deps.mjs diff --git a/.github/workflows/docker-build.yml b/.github/workflows/docker-build.yml index 7db238c4..227352ec 100644 --- a/.github/workflows/docker-build.yml +++ b/.github/workflows/docker-build.yml @@ -106,6 +106,16 @@ jobs: - name: Checkout repository uses: actions/checkout@v4 + - name: Generate tags + id: tags + run: | + # Sanitize branch name by replacing / with - + BRANCH_NAME=$(echo "${{ github.ref_name }}" | sed 's/\//-/g') + # Get first 4 characters of commit SHA + SHORT_SHA=$(echo "${{ github.sha }}" | cut -c1-4) + echo "branch_name=$BRANCH_NAME" >> $GITHUB_OUTPUT + echo "short_sha=$SHORT_SHA" >> $GITHUB_OUTPUT + - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 @@ -125,8 +135,7 @@ jobs: cache-from: type=gha cache-to: type=gha,mode=max tags: | - ghcr.io/${{ env.repo_owner }}/api:latest - ghcr.io/${{ env.repo_owner }}/api:${{ github.sha }} + ghcr.io/${{ env.repo_owner }}/api:${{ steps.tags.outputs.branch_name }}-${{ steps.tags.outputs.short_sha }} build-args: | DATABASE_URL=postgresql://dummy:dummy@localhost:5432/dummy @@ -140,6 +149,16 @@ jobs: - name: Checkout repository uses: actions/checkout@v4 + - name: Generate tags + id: tags + run: | + # Sanitize branch name by replacing / with - + BRANCH_NAME=$(echo "${{ github.ref_name }}" | sed 's/\//-/g') + # Get first 4 characters of commit SHA + SHORT_SHA=$(echo "${{ github.sha }}" | cut -c1-4) + echo "branch_name=$BRANCH_NAME" >> $GITHUB_OUTPUT + echo "short_sha=$SHORT_SHA" >> $GITHUB_OUTPUT + - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 @@ -159,7 +178,6 @@ jobs: cache-from: type=gha cache-to: type=gha,mode=max tags: | - ghcr.io/${{ env.repo_owner }}/worker:latest - ghcr.io/${{ env.repo_owner }}/worker:${{ github.sha }} + ghcr.io/${{ env.repo_owner }}/worker:${{ steps.tags.outputs.branch_name }}-${{ steps.tags.outputs.short_sha }} build-args: | DATABASE_URL=postgresql://dummy:dummy@localhost:5432/dummy diff --git a/.gitignore b/.gitignore index 96b7ab03..c2f17021 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ .secrets +packages/db/src/generated/prisma # Based on https://raw.githubusercontent.com/github/gitignore/main/Node.gitignore packages/sdk/profileId.txt @@ -168,6 +169,9 @@ dist .vscode-test +# Wrangler build artifacts and cache +.wrangler/ + # yarn v2 .yarn/cache diff --git a/apps/api/Dockerfile b/apps/api/Dockerfile index 292eda4c..a4efad54 100644 --- a/apps/api/Dockerfile +++ b/apps/api/Dockerfile @@ -1,4 +1,4 @@ -ARG NODE_VERSION=20.15.1 +ARG NODE_VERSION=22.20.0 FROM node:${NODE_VERSION}-slim AS base @@ -43,6 +43,7 @@ COPY packages/constants/package.json packages/constants/ COPY packages/validation/package.json packages/validation/ COPY packages/integrations/package.json packages/integrations/ COPY packages/sdks/sdk/package.json packages/sdks/sdk/ +COPY patches ./patches # BUILD FROM base AS build @@ -66,6 +67,8 @@ RUN pnpm codegen && \ # PROD FROM base AS prod +ENV npm_config_build_from_source=true + RUN apt-get update && apt-get install -y --no-install-recommends \ python3 \ make \ @@ -75,12 +78,14 @@ WORKDIR /app COPY --from=build /app/package.json ./ COPY --from=build /app/pnpm-lock.yaml ./ RUN pnpm install --frozen-lockfile --prod && \ + pnpm rebuild && \ pnpm store prune # FINAL FROM base AS runner ENV NODE_ENV=production +ENV npm_config_build_from_source=true WORKDIR /app @@ -106,6 +111,7 @@ COPY --from=build /app/packages/sdks/sdk ./packages/sdks/sdk COPY --from=build /app/packages/constants ./packages/constants COPY --from=build /app/packages/validation ./packages/validation COPY --from=build /app/packages/integrations ./packages/integrations +COPY --from=build /app/tooling/typescript ./tooling/typescript RUN pnpm db:codegen WORKDIR /app/apps/api diff --git a/apps/api/package.json b/apps/api/package.json index 06814391..e42cc243 100644 --- a/apps/api/package.json +++ b/apps/api/package.json @@ -1,11 +1,12 @@ { "name": "@openpanel/api", "version": "0.0.4", + "type": "module", "scripts": { - "dev": "dotenv -e ../../.env -c -v WATCH=1 tsup", + "dev": "dotenv -e ../../.env -c -v WATCH=1 tsdown", "testing": "API_PORT=3333 pnpm dev", - "start": "node dist/index.js", - "build": "rm -rf dist && tsup", + "start": "dotenv -e ../../.env node dist/index.js", + "build": "rm -rf dist && tsdown", "gen:bots": "jiti scripts/get-bots.ts && biome format --write src/bots/bots.ts", "typecheck": "tsc --noEmit" }, @@ -31,15 +32,13 @@ "@openpanel/redis": "workspace:*", "@openpanel/trpc": "workspace:*", "@openpanel/validation": "workspace:*", - "@trpc/server": "^10.45.2", + "@trpc/server": "^11.6.0", "ai": "^4.2.10", - "bcrypt": "^5.1.1", "fast-json-stable-hash": "^1.0.3", "fastify": "^5.2.1", "fastify-metrics": "^12.1.0", "fastify-raw-body": "^5.0.0", "groupmq": "1.0.0-next.19", - "ico-to-png": "^0.2.2", "jsonwebtoken": "^9.0.2", "ramda": "^0.29.1", "request-ip": "^3.3.0", @@ -65,7 +64,7 @@ "@types/uuid": "^10.0.0", "@types/ws": "^8.5.14", "js-yaml": "^4.1.0", - "tsup": "^7.2.0", - "typescript": "^5.2.2" + "tsdown": "^0.14.2", + "typescript": "catalog:" } } diff --git a/apps/api/scripts/get-bots.ts b/apps/api/scripts/get-bots.ts index f72561ff..992a0fe2 100644 --- a/apps/api/scripts/get-bots.ts +++ b/apps/api/scripts/get-bots.ts @@ -1,5 +1,10 @@ import fs from 'node:fs'; import path from 'node:path'; +import { dirname } from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = dirname(__filename); import yaml from 'js-yaml'; async function main() { diff --git a/apps/api/scripts/mock.ts b/apps/api/scripts/mock.ts index 0e61a55e..a4bdbf7c 100644 --- a/apps/api/scripts/mock.ts +++ b/apps/api/scripts/mock.ts @@ -493,9 +493,11 @@ async function main() { const [type, file = 'mock-basic.json'] = process.argv.slice(2); switch (type) { - case 'send': - await triggerEvents(require(`./${file}`)); + case 'send': { + const data = await import(`./${file}`, { assert: { type: 'json' } }); + await triggerEvents(data.default); break; + } case 'sim': await simultaneousRequests(); break; diff --git a/apps/api/src/controllers/ai.controller.ts b/apps/api/src/controllers/ai.controller.ts index 20631af6..6162d036 100644 --- a/apps/api/src/controllers/ai.controller.ts +++ b/apps/api/src/controllers/ai.controller.ts @@ -112,7 +112,7 @@ export async function chat( await db.chat.create({ data: { - messages: messagesToSave.slice(-10), + messages: messagesToSave.slice(-10) as any, projectId, }, }); diff --git a/apps/api/src/controllers/misc.controller.ts b/apps/api/src/controllers/misc.controller.ts index 99323e33..e441b484 100644 --- a/apps/api/src/controllers/misc.controller.ts +++ b/apps/api/src/controllers/misc.controller.ts @@ -1,53 +1,231 @@ +import crypto from 'node:crypto'; import { logger } from '@/utils/logger'; import { parseUrlMeta } from '@/utils/parseUrlMeta'; import type { FastifyReply, FastifyRequest } from 'fastify'; -import icoToPng from 'ico-to-png'; import sharp from 'sharp'; import { getClientIp } from '@/utils/get-client-ip'; -import { createHash } from '@openpanel/common/server'; import { TABLE_NAMES, ch, chQuery, formatClickhouseDate } from '@openpanel/db'; import { getGeoLocation } from '@openpanel/geo'; -import { cacheable, getCache, getRedisCache } from '@openpanel/redis'; +import { getCache, getRedisCache } from '@openpanel/redis'; interface GetFaviconParams { url: string; } -async function getImageBuffer(url: string) { +// Configuration +const TTL_SECONDS = 60 * 60 * 24; // 24h +const MAX_BYTES = 1_000_000; // 1MB cap +const USER_AGENT = 'OpenPanel-FaviconProxy/1.0 (+https://openpanel.dev)'; + +// Helper functions +function createCacheKey(url: string, prefix = 'favicon'): string { + const hash = crypto.createHash('sha256').update(url).digest('hex'); + return `${prefix}:v2:${hash}`; +} + +function validateUrl(raw?: string): URL | null { try { - const res = await fetch(url); - const contentType = res.headers.get('content-type'); + if (!raw) throw new Error('Missing ?url'); + const url = new URL(raw); + if (url.protocol !== 'http:' && url.protocol !== 'https:') { + throw new Error('Only http/https URLs are allowed'); + } + return url; + } catch (error) { + return null; + } +} - if (!contentType?.includes('image')) { - return null; +// Binary cache functions (more efficient than base64) +async function getFromCacheBinary( + key: string, +): Promise<{ buffer: Buffer; contentType: string } | null> { + const redis = getRedisCache(); + const [bufferBase64, contentType] = await Promise.all([ + redis.get(key), + redis.get(`${key}:ctype`), + ]); + + if (!bufferBase64 || !contentType) return null; + return { buffer: Buffer.from(bufferBase64, 'base64'), contentType }; +} + +async function setToCacheBinary( + key: string, + buffer: Buffer, + contentType: string, +): Promise { + const redis = getRedisCache(); + await Promise.all([ + redis.set(key, buffer.toString('base64'), 'EX', TTL_SECONDS), + redis.set(`${key}:ctype`, contentType, 'EX', TTL_SECONDS), + ]); +} + +// Fetch image with timeout and size limits +async function fetchImage( + url: URL, +): Promise<{ buffer: Buffer; contentType: string; status: number }> { + const controller = new AbortController(); + const timeout = setTimeout(() => controller.abort(), 10000); // 10s timeout + + try { + const response = await fetch(url.toString(), { + redirect: 'follow', + signal: controller.signal, + headers: { + 'user-agent': USER_AGENT, + accept: 'image/*,*/*;q=0.8', + }, + }); + + clearTimeout(timeout); + + if (!response.ok) { + return { + buffer: Buffer.alloc(0), + contentType: 'text/plain', + status: response.status, + }; } - if (!res.ok) { - return null; + // Size guard + const contentLength = Number(response.headers.get('content-length') ?? '0'); + if (contentLength > MAX_BYTES) { + throw new Error(`Remote file too large: ${contentLength} bytes`); } - if (contentType === 'image/x-icon' || url.endsWith('.ico')) { - const arrayBuffer = await res.arrayBuffer(); - const buffer = Buffer.from(arrayBuffer); - return await icoToPng(buffer, 30); + const arrayBuffer = await response.arrayBuffer(); + const buffer = Buffer.from(arrayBuffer); + + // Additional size check for actual content + if (buffer.length > MAX_BYTES) { + throw new Error('Remote file exceeded size limit'); } - return await sharp(await res.arrayBuffer()) + const contentType = + response.headers.get('content-type') || 'application/octet-stream'; + return { buffer, contentType, status: 200 }; + } catch (error) { + clearTimeout(timeout); + return { buffer: Buffer.alloc(0), contentType: 'text/plain', status: 500 }; + } +} + +// Check if URL is an ICO file +function isIcoFile(url: string, contentType?: string): boolean { + return url.toLowerCase().endsWith('.ico') || contentType === 'image/x-icon'; +} +function isSvgFile(url: string, contentType?: string): boolean { + return url.toLowerCase().endsWith('.svg') || contentType === 'image/svg+xml'; +} + +// Process image with Sharp (resize to 30x30 PNG) +async function processImage( + buffer: Buffer, + originalUrl?: string, + contentType?: string, +): Promise { + // If it's an ICO file, just return it as-is (no conversion needed) + if (originalUrl && isIcoFile(originalUrl, contentType)) { + logger.info('Serving ICO file directly', { + originalUrl, + bufferSize: buffer.length, + }); + return buffer; + } + + if (originalUrl && isSvgFile(originalUrl, contentType)) { + logger.info('Serving SVG file directly', { + originalUrl, + bufferSize: buffer.length, + }); + return buffer; + } + + // If buffer isnt to big just return it as well + if (buffer.length < 5000) { + logger.info('Serving image directly without processing', { + originalUrl, + bufferSize: buffer.length, + }); + return buffer; + } + + try { + // For other formats, process with Sharp + return await sharp(buffer) .resize(30, 30, { fit: 'cover', }) .png() .toBuffer(); } catch (error) { - logger.error('Failed to get image from url', { - error, - url, + logger.warn('Sharp failed to process image, trying fallback', { + error: error instanceof Error ? error.message : 'Unknown error', + originalUrl, + bufferSize: buffer.length, }); + + // If Sharp fails, try to create a simple fallback image + return createFallbackImage(); } } -const imageExtensions = ['svg', 'png', 'jpg', 'jpeg', 'gif', 'webp', 'ico']; +// Create a simple transparent fallback image when Sharp can't process the original +function createFallbackImage(): Buffer { + // 1x1 transparent PNG + return Buffer.from( + 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNk+A8AAQUBAScY42YAAAAASUVORK5CYII=', + 'base64', + ); +} + +// Process OG image with Sharp (resize to 300px width) +async function processOgImage( + buffer: Buffer, + originalUrl?: string, + contentType?: string, +): Promise { + // If buffer is small enough, return it as-is + if (buffer.length < 10000) { + logger.info('Serving OG image directly without processing', { + originalUrl, + bufferSize: buffer.length, + }); + return buffer; + } + + try { + // For OG images, process with Sharp to 300px width, maintaining aspect ratio + return await sharp(buffer) + .resize(300, null, { + fit: 'inside', + withoutEnlargement: true, + }) + .png() + .toBuffer(); + } catch (error) { + logger.warn('Sharp failed to process OG image, trying fallback', { + error: error instanceof Error ? error.message : 'Unknown error', + originalUrl, + bufferSize: buffer.length, + }); + + // If Sharp fails, try to create a simple fallback image + return createFallbackImage(); + } +} + +// Check if URL is a direct image +function isDirectImage(url: URL): boolean { + const imageExtensions = ['svg', 'png', 'jpg', 'jpeg', 'gif', 'webp', 'ico']; + return ( + imageExtensions.some((ext) => url.pathname.endsWith(`.${ext}`)) || + url.toString().includes('googleusercontent.com') + ); +} export async function getFavicon( request: FastifyRequest<{ @@ -55,68 +233,110 @@ export async function getFavicon( }>, reply: FastifyReply, ) { - function sendBuffer(buffer: Buffer, cacheKey?: string) { - if (cacheKey) { - getRedisCache().set(`favicon:${cacheKey}`, buffer.toString('base64')); + try { + const url = validateUrl(request.query.url); + if (!url) { + return createFallbackImage(); } - reply.header('Cache-Control', 'public, max-age=604800'); - reply.header('Expires', new Date(Date.now() + 604800000).toUTCString()); - reply.type('image/png'); - return reply.send(buffer); - } - if (!request.query.url) { - return reply.status(404).send('Not found'); - } + const cacheKey = createCacheKey(url.toString()); - const url = decodeURIComponent(request.query.url); - - if (imageExtensions.find((ext) => url.endsWith(ext))) { - const cacheKey = createHash(url, 32); - const cache = await getRedisCache().get(`favicon:${cacheKey}`); - if (cache) { - return sendBuffer(Buffer.from(cache, 'base64')); + // Check cache first + const cached = await getFromCacheBinary(cacheKey); + if (cached) { + reply.header('Content-Type', cached.contentType); + reply.header('Cache-Control', 'public, max-age=604800, immutable'); + return reply.send(cached.buffer); } - const buffer = await getImageBuffer(url); - if (buffer && buffer.byteLength > 0) { - return sendBuffer(buffer, cacheKey); + + let imageUrl: URL; + + // If it's a direct image URL, use it directly + if (isDirectImage(url)) { + imageUrl = url; + } else { + // For website URLs, extract favicon from HTML + const meta = await parseUrlMeta(url.toString()); + if (meta?.favicon) { + imageUrl = new URL(meta.favicon); + } else { + // Fallback to Google's favicon service + const { hostname } = url; + imageUrl = new URL( + `https://www.google.com/s2/favicons?domain=${hostname}&sz=256`, + ); + } } - } - const { hostname } = new URL(url); - const cache = await getRedisCache().get(`favicon:${hostname}`); + // Fetch the image + const { buffer, contentType, status } = await fetchImage(imageUrl); - if (cache) { - return sendBuffer(Buffer.from(cache, 'base64')); - } - - const meta = await parseUrlMeta(url); - if (meta?.favicon) { - const buffer = await getImageBuffer(meta.favicon); - if (buffer && buffer.byteLength > 0) { - return sendBuffer(buffer, hostname); + if (status !== 200 || buffer.length === 0) { + return reply.send(createFallbackImage()); } - } - const buffer = await getImageBuffer( - 'https://www.iconsdb.com/icons/download/orange/warning-128.png', - ); - if (buffer && buffer.byteLength > 0) { - return sendBuffer(buffer, hostname); - } + // Process the image (resize to 30x30 PNG, or serve ICO as-is) + const processedBuffer = await processImage( + buffer, + imageUrl.toString(), + contentType, + ); - return reply.status(404).send('Not found'); + // Determine the correct content type for caching and response + const isIco = isIcoFile(imageUrl.toString(), contentType); + const responseContentType = isIco ? 'image/x-icon' : contentType; + + // Cache the result with correct content type + await setToCacheBinary(cacheKey, processedBuffer, responseContentType); + + reply.header('Content-Type', responseContentType); + reply.header('Cache-Control', 'public, max-age=3600, immutable'); + return reply.send(processedBuffer); + } catch (error: any) { + logger.error('Favicon fetch error', { + error: error.message, + url: request.query.url, + }); + + const message = + process.env.NODE_ENV === 'production' + ? 'Bad request' + : (error?.message ?? 'Error'); + reply.header('Cache-Control', 'no-store'); + return reply.status(400).send(message); + } } export async function clearFavicons( request: FastifyRequest, reply: FastifyReply, ) { - const keys = await getRedisCache().keys('favicon:*'); + const redis = getRedisCache(); + const keys = await redis.keys('favicon:*'); + + // Delete both the binary data and content-type keys for (const key of keys) { - await getRedisCache().del(key); + await redis.del(key); + await redis.del(`${key}:ctype`); } - return reply.status(404).send('OK'); + + return reply.status(200).send('OK'); +} + +export async function clearOgImages( + request: FastifyRequest, + reply: FastifyReply, +) { + const redis = getRedisCache(); + const keys = await redis.keys('og:*'); + + // Delete both the binary data and content-type keys + for (const key of keys) { + await redis.del(key); + await redis.del(`${key}:ctype`); + } + + return reply.status(200).send('OK'); } export async function ping( @@ -181,3 +401,77 @@ export async function getGeo(request: FastifyRequest, reply: FastifyReply) { const geo = await getGeoLocation(ip); return reply.status(200).send(geo); } + +export async function getOgImage( + request: FastifyRequest<{ + Querystring: { + url: string; + }; + }>, + reply: FastifyReply, +) { + try { + const url = validateUrl(request.query.url); + if (!url) { + return getFavicon(request, reply); + } + const cacheKey = createCacheKey(url.toString(), 'og'); + + // Check cache first + const cached = await getFromCacheBinary(cacheKey); + if (cached) { + reply.header('Content-Type', cached.contentType); + reply.header('Cache-Control', 'public, max-age=604800, immutable'); + return reply.send(cached.buffer); + } + + let imageUrl: URL; + + // If it's a direct image URL, use it directly + if (isDirectImage(url)) { + imageUrl = url; + } else { + // For website URLs, extract OG image from HTML + const meta = await parseUrlMeta(url.toString()); + if (meta?.ogImage) { + imageUrl = new URL(meta.ogImage); + } else { + // No OG image found, return a fallback + return getFavicon(request, reply); + } + } + + // Fetch the image + const { buffer, contentType, status } = await fetchImage(imageUrl); + + if (status !== 200 || buffer.length === 0) { + return getFavicon(request, reply); + } + + // Process the image (resize to 1200x630 for OG standards, or serve as-is if reasonable size) + const processedBuffer = await processOgImage( + buffer, + imageUrl.toString(), + contentType, + ); + + // Cache the result + await setToCacheBinary(cacheKey, processedBuffer, 'image/png'); + + reply.header('Content-Type', 'image/png'); + reply.header('Cache-Control', 'public, max-age=3600, immutable'); + return reply.send(processedBuffer); + } catch (error: any) { + logger.error('OG image fetch error', { + error: error.message, + url: request.query.url, + }); + + const message = + process.env.NODE_ENV === 'production' + ? 'Bad request' + : (error?.message ?? 'Error'); + reply.header('Cache-Control', 'no-store'); + return reply.status(400).send(message); + } +} diff --git a/apps/api/src/controllers/oauth-callback.controller.tsx b/apps/api/src/controllers/oauth-callback.controller.tsx index 2b45adcc..77e79eca 100644 --- a/apps/api/src/controllers/oauth-callback.controller.tsx +++ b/apps/api/src/controllers/oauth-callback.controller.tsx @@ -76,7 +76,9 @@ async function handleExistingUser({ sessionToken, session.expiresAt, ); - return reply.redirect(process.env.NEXT_PUBLIC_DASHBOARD_URL!); + return reply.redirect( + process.env.DASHBOARD_URL || process.env.NEXT_PUBLIC_DASHBOARD_URL!, + ); } async function handleNewUser({ @@ -138,7 +140,9 @@ async function handleNewUser({ sessionToken, session.expiresAt, ); - return reply.redirect(process.env.NEXT_PUBLIC_DASHBOARD_URL!); + return reply.redirect( + process.env.DASHBOARD_URL || process.env.NEXT_PUBLIC_DASHBOARD_URL!, + ); } // Provider-specific user fetching @@ -348,7 +352,9 @@ export async function googleCallback(req: FastifyRequest, reply: FastifyReply) { } function redirectWithError(reply: FastifyReply, error: LogError | unknown) { - const url = new URL(process.env.NEXT_PUBLIC_DASHBOARD_URL!); + const url = new URL( + process.env.DASHBOARD_URL || process.env.NEXT_PUBLIC_DASHBOARD_URL!, + ); url.pathname = '/login'; if (error instanceof LogError) { url.searchParams.set('error', error.message); diff --git a/apps/api/src/controllers/webhook.controller.ts b/apps/api/src/controllers/webhook.controller.ts index 70db830b..36785c44 100644 --- a/apps/api/src/controllers/webhook.controller.ts +++ b/apps/api/src/controllers/webhook.controller.ts @@ -1,5 +1,10 @@ import fs from 'node:fs'; import path from 'node:path'; +import { dirname } from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = dirname(__filename); import { db, getOrganizationByProjectIdCached } from '@openpanel/db'; import { sendSlackNotification, @@ -100,7 +105,7 @@ export async function slackWebhook( }); return reply.redirect( - `${process.env.NEXT_PUBLIC_DASHBOARD_URL}/${organizationId}/${projectId}/settings/integrations?tab=installed`, + `${process.env.DASHBOARD_URL || process.env.NEXT_PUBLIC_DASHBOARD_URL}/${organizationId}/${projectId}/settings/integrations?tab=installed`, ); } catch (err) { request.log.error(err); @@ -184,7 +189,7 @@ export async function polarWebhook( data: { subscriptionId: event.data.id, subscriptionCustomerId: event.data.customer.id, - subscriptionPriceId: event.data.priceId, + subscriptionPriceId: event.data.prices[0]?.id ?? null, subscriptionProductId: event.data.productId, subscriptionStatus: event.data.status, subscriptionStartsAt: event.data.currentPeriodStart, diff --git a/apps/api/src/hooks/request-logging.hook.ts b/apps/api/src/hooks/request-logging.hook.ts index 5f3a8150..3d2c9961 100644 --- a/apps/api/src/hooks/request-logging.hook.ts +++ b/apps/api/src/hooks/request-logging.hook.ts @@ -7,7 +7,7 @@ const ignoreMethods = ['OPTIONS']; const getTrpcInput = ( request: FastifyRequest, ): Record | undefined => { - const input = path(['query', 'input'], request); + const input = path(['query', 'input'], request); try { return typeof input === 'string' ? JSON.parse(input).json : input; } catch (e) { diff --git a/apps/api/src/index.ts b/apps/api/src/index.ts index e960f0d9..83680b72 100644 --- a/apps/api/src/index.ts +++ b/apps/api/src/index.ts @@ -95,15 +95,13 @@ const startServer = async () => { if (isPrivatePath) { // Allow multiple dashboard domains const allowedOrigins = [ - process.env.NEXT_PUBLIC_DASHBOARD_URL, + process.env.DASHBOARD_URL || process.env.NEXT_PUBLIC_DASHBOARD_URL, ...(process.env.API_CORS_ORIGINS?.split(',') ?? []), ].filter(Boolean); const origin = req.headers.origin; const isAllowed = origin && allowedOrigins.includes(origin); - logger.info('Allowed origins', { allowedOrigins, origin, isAllowed }); - return callback(null, { origin: isAllowed ? origin : false, credentials: true, @@ -160,6 +158,12 @@ const startServer = async () => { router: appRouter, createContext: createContext, onError(ctx) { + if ( + ctx.error.code === 'UNAUTHORIZED' && + ctx.path === 'organization.list' + ) { + return; + } ctx.req.log.error('trpc error', { error: ctx.error, path: ctx.path, @@ -191,7 +195,10 @@ const startServer = async () => { instance.get('/healthz/live', liveness); instance.get('/healthz/ready', readiness); instance.get('/', (_request, reply) => - reply.send({ name: 'openpanel sdk api' }), + reply.send({ + status: 'ok', + message: 'Successfully running OpenPanel.dev API', + }), ); }); diff --git a/apps/api/src/routes/misc.router.ts b/apps/api/src/routes/misc.router.ts index 842c85d4..db5ffba6 100644 --- a/apps/api/src/routes/misc.router.ts +++ b/apps/api/src/routes/misc.router.ts @@ -20,6 +20,18 @@ const miscRouter: FastifyPluginCallback = async (fastify) => { handler: controller.getFavicon, }); + fastify.route({ + method: 'GET', + url: '/og', + handler: controller.getOgImage, + }); + + fastify.route({ + method: 'GET', + url: '/og/clear', + handler: controller.clearOgImages, + }); + fastify.route({ method: 'GET', url: '/favicon/clear', diff --git a/apps/api/src/utils/parseUrlMeta.ts b/apps/api/src/utils/parseUrlMeta.ts index cda031a0..b9de388c 100644 --- a/apps/api/src/utils/parseUrlMeta.ts +++ b/apps/api/src/utils/parseUrlMeta.ts @@ -5,12 +5,16 @@ function fallbackFavicon(url: string) { } function findBestFavicon(favicons: UrlMetaData['favicons']) { - const match = favicons.find( - (favicon) => - favicon.rel === 'shortcut icon' || - favicon.rel === 'icon' || - favicon.rel === 'apple-touch-icon', - ); + const match = favicons + .sort((a, b) => { + return a.rel.length - b.rel.length; + }) + .find( + (favicon) => + favicon.rel === 'shortcut icon' || + favicon.rel === 'icon' || + favicon.rel === 'apple-touch-icon', + ); if (match) { return match.href; @@ -18,11 +22,32 @@ function findBestFavicon(favicons: UrlMetaData['favicons']) { return null; } +function findBestOgImage(data: UrlMetaData): string | null { + // Priority order for OG images + const candidates = [ + data['og:image:secure_url'], + data['og:image:url'], + data['og:image'], + data['twitter:image:src'], + data['twitter:image'], + ]; + + for (const candidate of candidates) { + if (candidate?.trim()) { + return candidate.trim(); + } + } + + return null; +} + function transform(data: UrlMetaData, url: string) { const favicon = findBestFavicon(data.favicons); + const ogImage = findBestOgImage(data); return { favicon: favicon ? new URL(favicon, url).toString() : fallbackFavicon(url), + ogImage: ogImage ? new URL(ogImage, url).toString() : null, }; } @@ -32,6 +57,11 @@ interface UrlMetaData { href: string; sizes: string; }[]; + 'og:image'?: string; + 'og:image:url'?: string; + 'og:image:secure_url'?: string; + 'twitter:image'?: string; + 'twitter:image:src'?: string; } export async function parseUrlMeta(url: string) { @@ -42,6 +72,7 @@ export async function parseUrlMeta(url: string) { } catch (err) { return { favicon: fallbackFavicon(url), + ogImage: null, }; } } diff --git a/apps/api/tsdown.config.ts b/apps/api/tsdown.config.ts new file mode 100644 index 00000000..80837b37 --- /dev/null +++ b/apps/api/tsdown.config.ts @@ -0,0 +1,23 @@ +import { defineConfig } from 'tsdown'; +import type { Options } from 'tsdown'; + +const options: Options = { + clean: true, + entry: ['src/index.ts'], + noExternal: [/^@openpanel\/.*$/u, /^@\/.*$/u], + external: ['@hyperdx/node-opentelemetry', 'winston', '@node-rs/argon2'], + sourcemap: true, + platform: 'node', + shims: true, + inputOptions: { + jsx: 'react', + }, +}; + +if (process.env.WATCH) { + options.watch = ['src', '../../packages']; + options.onSuccess = 'node --enable-source-maps dist/index.js'; + options.minify = false; +} + +export default defineConfig(options); diff --git a/apps/api/tsup.config.ts b/apps/api/tsup.config.ts deleted file mode 100644 index d1bf3ea8..00000000 --- a/apps/api/tsup.config.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { defineConfig } from 'tsup'; -import type { Options } from 'tsup'; - -const options: Options = { - clean: true, - entry: ['src/index.ts'], - noExternal: [/^@openpanel\/.*$/u, /^@\/.*$/u], - external: [ - '@hyperdx/node-opentelemetry', - 'winston', - '@node-rs/argon2', - 'bcrypt', - ], - ignoreWatch: ['../../**/{.git,node_modules,dist}/**'], - sourcemap: true, - splitting: false, -}; - -if (process.env.WATCH) { - options.watch = ['src/**/*', '../../packages/**/*']; - - options.onSuccess = 'node dist/index.js'; - options.minify = false; -} - -export default defineConfig(options); diff --git a/apps/dashboard/.gitignore b/apps/dashboard/.gitignore deleted file mode 100644 index 04424e48..00000000 --- a/apps/dashboard/.gitignore +++ /dev/null @@ -1,39 +0,0 @@ -# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. - -# dependencies -/node_modules -/.pnp -.pnp.js - -# testing -/coverage - -# database -/prisma/db.sqlite -/prisma/db.sqlite-journal - -# next.js -/.next/ -/out/ -next-env.d.ts - -# production -/build - -# misc -.DS_Store -*.pem - -# debug -npm-debug.log* - -# local env files -# do not commit any .env files to git, except for the .env.example file. https://create.t3.gg/en/usage/env-variables#using-environment-variables -.env -.env*.local - -# vercel -.vercel - -# typescript -*.tsbuildinfo diff --git a/apps/dashboard/.sentryclirc b/apps/dashboard/.sentryclirc deleted file mode 100644 index ddbffd3c..00000000 --- a/apps/dashboard/.sentryclirc +++ /dev/null @@ -1,3 +0,0 @@ - -[auth] -token=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 diff --git a/apps/dashboard/Dockerfile b/apps/dashboard/Dockerfile deleted file mode 100644 index 3d25011e..00000000 --- a/apps/dashboard/Dockerfile +++ /dev/null @@ -1,103 +0,0 @@ -ARG NODE_VERSION=20.15.1 - -FROM node:${NODE_VERSION}-slim AS base - -# FIX: Bad workaround (https://github.com/nodejs/corepack/issues/612) -ENV COREPACK_INTEGRITY_KEYS=0 - -ENV SKIP_ENV_VALIDATION="1" - -ARG DATABASE_URL -ENV DATABASE_URL=$DATABASE_URL - -ARG ENABLE_INSTRUMENTATION_HOOK -ENV ENABLE_INSTRUMENTATION_HOOK=$ENABLE_INSTRUMENTATION_HOOK - -ARG NEXT_PUBLIC_SELF_HOSTED -ENV NEXT_PUBLIC_SELF_HOSTED=$NEXT_PUBLIC_SELF_HOSTED - -ENV PNPM_HOME="/pnpm" -ENV PATH="$PNPM_HOME:$PATH" - -# Install necessary dependencies for prisma -RUN apt-get update && apt-get install -y \ - openssl \ - libssl3 \ - curl \ - && rm -rf /var/lib/apt/lists/* - -RUN corepack enable - -WORKDIR /app - -ARG CACHE_BUST -RUN echo "CACHE BUSTER: $CACHE_BUST" - -COPY package.json package.json -COPY pnpm-lock.yaml pnpm-lock.yaml -COPY pnpm-workspace.yaml pnpm-workspace.yaml -COPY apps/dashboard/package.json apps/dashboard/package.json -COPY packages/db/package.json packages/db/package.json -COPY packages/json/package.json packages/json/package.json -COPY packages/redis/package.json packages/redis/package.json -COPY packages/queue/package.json packages/queue/package.json -COPY packages/common/package.json packages/common/package.json -COPY packages/auth/package.json packages/auth/package.json -COPY packages/email/package.json packages/email/package.json -COPY packages/constants/package.json packages/constants/package.json -COPY packages/payments/package.json packages/payments/package.json -COPY packages/validation/package.json packages/validation/package.json -COPY packages/integrations/package.json packages/integrations/package.json -COPY packages/sdks/sdk/package.json packages/sdks/sdk/package.json - -# BUILD -FROM base AS build - -WORKDIR /app -RUN pnpm install --frozen-lockfile --ignore-scripts - -COPY apps/dashboard apps/dashboard -COPY packages packages -COPY tooling tooling -RUN pnpm db:codegen - -WORKDIR /app/apps/dashboard - -# Will be replaced on runtime -ENV NEXT_PUBLIC_DASHBOARD_URL="__NEXT_PUBLIC_DASHBOARD_URL__" -ENV NEXT_PUBLIC_API_URL="__NEXT_PUBLIC_API_URL__" - -RUN pnpm run build - -# RUNNER -FROM base AS runner - -WORKDIR /app - -ENV NODE_ENV=production -ENV NEXT_TELEMETRY_DISABLED=1 - -RUN addgroup --system --gid 1001 nodejs -RUN adduser --system --uid 1001 nextjs - -# Set the correct permission for prerender cache -RUN mkdir .next -RUN chown nextjs:nodejs .next - -# Set the correct permissions for the entire /app directory -COPY --from=build --chown=nextjs:nodejs /app/apps/dashboard/.next/standalone ./ -COPY --from=build --chown=nextjs:nodejs /app/apps/dashboard/.next/static ./apps/dashboard/.next/static -COPY --from=build --chown=nextjs:nodejs /app/apps/dashboard/public ./apps/dashboard/public - -# Copy and set permissions for the entrypoint script -COPY --from=build --chown=nextjs:nodejs /app/apps/dashboard/entrypoint.sh ./entrypoint.sh -RUN chmod +x ./entrypoint.sh - -USER nextjs - -EXPOSE 3000 - -ENV PORT=3000 -ENV HOSTNAME=0.0.0.0 - -ENTRYPOINT [ "/app/entrypoint.sh", "node", "/app/apps/dashboard/server.js"] \ No newline at end of file diff --git a/apps/dashboard/README.md b/apps/dashboard/README.md deleted file mode 100644 index 5c6b25c4..00000000 --- a/apps/dashboard/README.md +++ /dev/null @@ -1 +0,0 @@ -# Dashboard diff --git a/apps/dashboard/components.json b/apps/dashboard/components.json deleted file mode 100644 index 9750ef2a..00000000 --- a/apps/dashboard/components.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "$schema": "https://ui.shadcn.com/schema.json", - "style": "default", - "rsc": false, - "tsx": true, - "tailwind": { - "config": "tailwind.config.ts", - "css": "src/styles/globals.css", - "baseColor": "slate", - "cssVariables": true - }, - "aliases": { - "components": "@/components", - "utils": "@/utils/cn" - } -} diff --git a/apps/dashboard/entrypoint.sh b/apps/dashboard/entrypoint.sh deleted file mode 100644 index 666f6eb5..00000000 --- a/apps/dashboard/entrypoint.sh +++ /dev/null @@ -1,33 +0,0 @@ -#!/bin/sh -set -e - -echo "> Replace env variable placeholders with runtime values..." - -# Define environment variables to check (space-separated string) -variables_to_replace="NEXT_PUBLIC_DASHBOARD_URL NEXT_PUBLIC_API_URL NEXT_PUBLIC_SELF_HOSTED" - -# Replace env variable placeholders with real values -for key in $variables_to_replace; do - value=$(eval echo \$"$key") - if [ -n "$value" ]; then - echo " - Searching for $key with value $value..." - # Use standard placeholder format for all variables - placeholder="__${key}__" - - # Run the replacement - find /app -type f \( -name "*.js" -o -name "*.html" \) | while read -r file; do - if grep -q "$placeholder" "$file"; then - echo " - Replacing in file: $file" - sed -i "s|$placeholder|$value|g" "$file" - fi - done - else - echo " - Skipping $key as it has no value set." - fi -done - -echo "> Done!" -echo "> Running $@" - -# Execute the container's main process (CMD in Dockerfile) -exec "$@" \ No newline at end of file diff --git a/apps/dashboard/next.config.mjs b/apps/dashboard/next.config.mjs deleted file mode 100644 index 6553dbba..00000000 --- a/apps/dashboard/next.config.mjs +++ /dev/null @@ -1,47 +0,0 @@ -// @ts-expect-error -import { PrismaPlugin } from '@prisma/nextjs-monorepo-workaround-plugin'; - -/** @type {import("next").NextConfig} */ -const config = { - output: 'standalone', - webpack: (config, { isServer }) => { - if (isServer) { - config.plugins = [...config.plugins, new PrismaPlugin()]; - } - - return config; - }, - reactStrictMode: true, - transpilePackages: [ - '@openpanel/queue', - '@openpanel/db', - '@openpanel/common', - '@openpanel/constants', - '@openpanel/redis', - '@openpanel/validation', - '@openpanel/email', - ], - eslint: { ignoreDuringBuilds: true }, - typescript: { ignoreBuildErrors: true }, - experimental: { - // Avoid "Critical dependency: the request of a dependency is an expression" - serverComponentsExternalPackages: [ - 'bullmq', - 'ioredis', - '@hyperdx/node-opentelemetry', - '@node-rs/argon2', - ], - instrumentationHook: !!process.env.ENABLE_INSTRUMENTATION_HOOK, - }, - /** - * If you are using `appDir` then you must comment the below `i18n` config out. - * - * @see https://github.com/vercel/next.js/issues/41980 - */ - i18n: { - locales: ['en'], - defaultLocale: 'en', - }, -}; - -export default config; diff --git a/apps/dashboard/package.json b/apps/dashboard/package.json deleted file mode 100644 index 372d9b18..00000000 --- a/apps/dashboard/package.json +++ /dev/null @@ -1,149 +0,0 @@ -{ - "name": "@openpanel/dashboard", - "version": "0.1.0", - "private": true, - "scripts": { - "dev": "rm -rf .next && pnpm with-env next dev", - "testing": "pnpm dev", - "build": "pnpm with-env next build", - "start": "pnpm with-env next start", - "typecheck": "tsc --noEmit", - "with-env": "dotenv -e ../../.env -c --" - }, - "dependencies": { - "@ai-sdk/react": "^1.2.5", - "@clickhouse/client": "^1.2.0", - "@dnd-kit/core": "^6.3.1", - "@dnd-kit/sortable": "^10.0.0", - "@dnd-kit/utilities": "^3.2.2", - "@hookform/resolvers": "^3.3.4", - "@hyperdx/node-opentelemetry": "^0.8.1", - "@openpanel/auth": "workspace:^", - "@openpanel/common": "workspace:^", - "@openpanel/constants": "workspace:^", - "@openpanel/db": "workspace:^", - "@openpanel/integrations": "workspace:^", - "@openpanel/json": "workspace:*", - "@openpanel/nextjs": "1.0.3", - "@openpanel/queue": "workspace:^", - "@openpanel/sdk-info": "workspace:^", - "@openpanel/validation": "workspace:^", - "@prisma/nextjs-monorepo-workaround-plugin": "^5.12.1", - "@radix-ui/react-accordion": "^1.1.2", - "@radix-ui/react-alert-dialog": "^1.0.5", - "@radix-ui/react-aspect-ratio": "^1.0.3", - "@radix-ui/react-avatar": "^1.0.4", - "@radix-ui/react-checkbox": "^1.0.4", - "@radix-ui/react-dialog": "^1.0.5", - "@radix-ui/react-dropdown-menu": "^2.0.6", - "@radix-ui/react-label": "^2.0.2", - "@radix-ui/react-popover": "^1.0.7", - "@radix-ui/react-portal": "^1.1.1", - "@radix-ui/react-progress": "^1.0.3", - "@radix-ui/react-radio-group": "^1.1.3", - "@radix-ui/react-scroll-area": "^1.0.5", - "@radix-ui/react-slot": "^1.0.2", - "@radix-ui/react-switch": "^1.0.3", - "@radix-ui/react-tabs": "^1.0.4", - "@radix-ui/react-toast": "^1.1.5", - "@radix-ui/react-toggle": "^1.0.3", - "@radix-ui/react-toggle-group": "^1.0.4", - "@radix-ui/react-tooltip": "^1.0.7", - "@reduxjs/toolkit": "^1.9.7", - "@t3-oss/env-nextjs": "^0.7.3", - "@tailwindcss/container-queries": "^0.1.1", - "@tailwindcss/typography": "^0.5.15", - "@tanstack/react-query": "^4.36.1", - "@tanstack/react-table": "^8.11.8", - "@tanstack/react-virtual": "^3.13.2", - "@trpc/client": "^10.45.2", - "@trpc/next": "^10.45.2", - "@trpc/react-query": "^10.45.2", - "@trpc/server": "^10.45.2", - "@types/d3": "^7.4.3", - "ai": "^4.2.10", - "bcrypt": "^5.1.1", - "bind-event-listener": "^3.0.0", - "class-variance-authority": "^0.7.0", - "clsx": "^2.1.0", - "cmdk": "^0.2.1", - "d3": "^7.8.5", - "date-fns": "^3.3.1", - "embla-carousel-react": "8.0.0-rc22", - "flag-icons": "^7.1.0", - "framer-motion": "^11.0.28", - "geist": "^1.3.1", - "hamburger-react": "^2.5.0", - "input-otp": "^1.2.4", - "javascript-time-ago": "^2.5.9", - "katex": "^0.16.21", - "lodash.debounce": "^4.0.8", - "lodash.isequal": "^4.5.0", - "lodash.throttle": "^4.1.1", - "lottie-react": "^2.4.0", - "lucide-react": "^0.513.0", - "mathjs": "^12.3.2", - "mitt": "^3.0.1", - "next": "14.2.1", - "next-auth": "^4.24.5", - "next-themes": "^0.2.1", - "nextjs-toploader": "^1.6.11", - "nuqs": "^2.0.2", - "prisma-error-enum": "^0.1.3", - "pushmodal": "^1.0.3", - "ramda": "^0.29.1", - "random-animal-name": "^0.1.1", - "rc-virtual-list": "^3.14.5", - "react": "18.2.0", - "react-animate-height": "^3.2.3", - "react-animated-numbers": "^0.18.0", - "react-day-picker": "^8.10.0", - "react-dom": "18.2.0", - "react-hook-form": "^7.50.1", - "react-in-viewport": "1.0.0-alpha.30", - "react-markdown": "^10.1.0", - "react-redux": "^8.1.3", - "react-responsive": "^9.0.2", - "react-simple-maps": "3.0.0", - "react-svg-worldmap": "2.0.0-alpha.16", - "react-syntax-highlighter": "^15.5.0", - "react-use-websocket": "^4.7.0", - "react-virtualized-auto-sizer": "^1.0.22", - "recharts": "^2.12.0", - "rehype-katex": "^7.0.1", - "remark-gfm": "^4.0.1", - "remark-highlight": "^0.1.1", - "remark-math": "^6.0.0", - "remark-parse": "^11.0.0", - "remark-rehype": "^11.1.2", - "short-unique-id": "^5.0.3", - "slugify": "^1.6.6", - "sonner": "^1.4.0", - "sqlstring": "^2.3.3", - "superjson": "^1.13.3", - "tailwind-merge": "^1.14.0", - "tailwindcss-animate": "^1.0.7", - "usehooks-ts": "^2.14.0", - "zod": "catalog:" - }, - "devDependencies": { - "@openpanel/payments": "workspace:*", - "@openpanel/trpc": "workspace:*", - "@openpanel/tsconfig": "workspace:*", - "@types/bcrypt": "^5.0.2", - "@types/lodash.debounce": "^4.0.9", - "@types/lodash.isequal": "^4.5.8", - "@types/lodash.throttle": "^4.1.9", - "@types/node": "20.14.8", - "@types/ramda": "^0.29.10", - "@types/react": "^18.2.20", - "@types/react-dom": "^18.2.7", - "@types/react-simple-maps": "^3.0.4", - "@types/react-syntax-highlighter": "^15.5.11", - "@types/sqlstring": "^2.3.2", - "autoprefixer": "^10.4.17", - "postcss": "^8.4.35", - "tailwindcss": "^3.4.1", - "typescript": "^5.3.3" - } -} diff --git a/apps/dashboard/postcss.config.cjs b/apps/dashboard/postcss.config.cjs deleted file mode 100644 index e305dd92..00000000 --- a/apps/dashboard/postcss.config.cjs +++ /dev/null @@ -1,8 +0,0 @@ -const config = { - plugins: { - tailwindcss: {}, - autoprefixer: {}, - }, -}; - -module.exports = config; diff --git a/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/chat/page.tsx b/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/chat/page.tsx deleted file mode 100644 index 57dc2280..00000000 --- a/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/chat/page.tsx +++ /dev/null @@ -1,31 +0,0 @@ -import Chat from '@/components/chat/chat'; -import { db, getOrganizationById } from '@openpanel/db'; -import type { UIMessage } from 'ai'; - -export default async function ChatPage({ - params, -}: { - params: { organizationSlug: string; projectId: string }; -}) { - const { projectId } = await params; - const [organization, chat] = await Promise.all([ - getOrganizationById(params.organizationSlug), - db.chat.findFirst({ - where: { - projectId, - }, - orderBy: { - createdAt: 'desc', - }, - }), - ]); - - const messages = ((chat?.messages as UIMessage[]) || []).slice(-10); - return ( - - ); -} diff --git a/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/dashboards/[dashboardId]/list-reports.tsx b/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/dashboards/[dashboardId]/list-reports.tsx deleted file mode 100644 index b6d322d6..00000000 --- a/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/dashboards/[dashboardId]/list-reports.tsx +++ /dev/null @@ -1,181 +0,0 @@ -'use client'; - -import { FullPageEmptyState } from '@/components/full-page-empty-state'; -import { useOverviewOptions } from '@/components/overview/useOverviewOptions'; -import { ReportChart } from '@/components/report-chart'; -import { Button } from '@/components/ui/button'; -import { - DropdownMenu, - DropdownMenuContent, - DropdownMenuGroup, - DropdownMenuItem, - DropdownMenuTrigger, -} from '@/components/ui/dropdown-menu'; -import { useAppParams } from '@/hooks/useAppParams'; -import { api, handleError } from '@/trpc/client'; -import { cn } from '@/utils/cn'; -import { - ChevronRight, - LayoutPanelTopIcon, - MoreHorizontal, - PlusIcon, - Trash, -} from 'lucide-react'; -import Link from 'next/link'; -import { useRouter } from 'next/navigation'; -import { toast } from 'sonner'; - -import { - getDefaultIntervalByDates, - getDefaultIntervalByRange, - timeWindows, -} from '@openpanel/constants'; -import type { IServiceDashboard, getReportsByDashboardId } from '@openpanel/db'; - -import { OverviewInterval } from '@/components/overview/overview-interval'; -import { OverviewRange } from '@/components/overview/overview-range'; - -interface ListReportsProps { - reports: Awaited>; - dashboard: IServiceDashboard; -} - -export function ListReports({ reports, dashboard }: ListReportsProps) { - const router = useRouter(); - const params = useAppParams<{ dashboardId: string }>(); - const { range, startDate, endDate, interval } = useOverviewOptions(); - const deletion = api.report.delete.useMutation({ - onError: handleError, - onSuccess() { - router.refresh(); - toast('Report deleted'); - }, - }); - return ( - <> -
-

{dashboard.name}

-
- - - -
-
-
- {reports.map((report) => { - const chartRange = report.range; - return ( -
- -
-
{report.name}
- {chartRange !== null && ( -
- - {timeWindows[chartRange].label} - - {startDate && endDate ? ( - Custom dates - ) : ( - range !== null && - chartRange !== range && ( - {timeWindows[range].label} - ) - )} -
- )} -
-
- - - - - - - { - event.stopPropagation(); - deletion.mutate({ - reportId: report.id, - }); - }} - > - - Delete - - - - - -
- -
- -
-
- ); - })} - {reports.length === 0 && ( - -

You can visualize your data with a report

- -
- )} -
- - ); -} diff --git a/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/dashboards/[dashboardId]/page.tsx b/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/dashboards/[dashboardId]/page.tsx deleted file mode 100644 index 5b5ef077..00000000 --- a/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/dashboards/[dashboardId]/page.tsx +++ /dev/null @@ -1,32 +0,0 @@ -import { Padding } from '@/components/ui/padding'; -import { notFound } from 'next/navigation'; - -import { getDashboardById, getReportsByDashboardId } from '@openpanel/db'; - -import { ListReports } from './list-reports'; - -interface PageProps { - params: { - projectId: string; - dashboardId: string; - }; -} - -export default async function Page({ - params: { projectId, dashboardId }, -}: PageProps) { - const [dashboard, reports] = await Promise.all([ - getDashboardById(dashboardId, projectId), - getReportsByDashboardId(dashboardId), - ]); - - if (!dashboard) { - return notFound(); - } - - return ( - - - - ); -} diff --git a/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/dashboards/list-dashboards/header.tsx b/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/dashboards/list-dashboards/header.tsx deleted file mode 100644 index 9ddc769a..00000000 --- a/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/dashboards/list-dashboards/header.tsx +++ /dev/null @@ -1,22 +0,0 @@ -'use client'; - -import { Button } from '@/components/ui/button'; -import { pushModal } from '@/modals'; -import { PlusIcon } from 'lucide-react'; - -export function HeaderDashboards() { - return ( -
-

Dashboards

- -
- ); -} diff --git a/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/dashboards/list-dashboards/index.tsx b/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/dashboards/list-dashboards/index.tsx deleted file mode 100644 index 30c13f12..00000000 --- a/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/dashboards/list-dashboards/index.tsx +++ /dev/null @@ -1,25 +0,0 @@ -import FullPageLoadingState from '@/components/full-page-loading-state'; -import { Padding } from '@/components/ui/padding'; -import withSuspense from '@/hocs/with-suspense'; - -import { getDashboardsByProjectId } from '@openpanel/db'; - -import { HeaderDashboards } from './header'; -import { ListDashboards } from './list-dashboards'; - -interface Props { - projectId: string; -} - -const ListDashboardsServer = async ({ projectId }: Props) => { - const dashboards = await getDashboardsByProjectId(projectId); - - return ( - - - - - ); -}; - -export default withSuspense(ListDashboardsServer, FullPageLoadingState); diff --git a/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/dashboards/page.tsx b/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/dashboards/page.tsx deleted file mode 100644 index 8787f9c2..00000000 --- a/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/dashboards/page.tsx +++ /dev/null @@ -1,11 +0,0 @@ -import ListDashboardsServer from './list-dashboards'; - -interface PageProps { - params: { - projectId: string; - }; -} - -export default function Page({ params: { projectId } }: PageProps) { - return ; -} diff --git a/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/events/conversions.tsx b/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/events/conversions.tsx deleted file mode 100644 index 30725de5..00000000 --- a/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/events/conversions.tsx +++ /dev/null @@ -1,43 +0,0 @@ -'use client'; - -import { TableButtons } from '@/components/data-table'; -import { EventsTable } from '@/components/events/table'; -import { EventsTableColumns } from '@/components/events/table/events-table-columns'; -import { api } from '@/trpc/client'; -import { Loader2Icon } from 'lucide-react'; - -type Props = { - projectId: string; - profileId?: string; -}; - -const Conversions = ({ projectId }: Props) => { - const query = api.event.conversions.useInfiniteQuery( - { - projectId, - }, - { - getNextPageParam: (lastPage) => lastPage.meta.next, - keepPreviousData: true, - }, - ); - - return ( -
- - - {query.isRefetching && ( -
- -
- )} -
- -
- ); -}; - -export default Conversions; diff --git a/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/events/events.tsx b/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/events/events.tsx deleted file mode 100644 index ab612730..00000000 --- a/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/events/events.tsx +++ /dev/null @@ -1,93 +0,0 @@ -'use client'; - -import { TableButtons } from '@/components/data-table'; -import EventListener from '@/components/events/event-listener'; -import { EventsTable } from '@/components/events/table'; -import { EventsTableColumns } from '@/components/events/table/events-table-columns'; -import { OverviewFiltersButtons } from '@/components/overview/filters/overview-filters-buttons'; -import { OverviewFiltersDrawer } from '@/components/overview/filters/overview-filters-drawer'; -import { Button } from '@/components/ui/button'; -import { - useEventQueryFilters, - useEventQueryNamesFilter, -} from '@/hooks/useEventQueryFilters'; -import { pushModal } from '@/modals'; -import { api } from '@/trpc/client'; -import { format } from 'date-fns'; -import { CalendarIcon, Loader2Icon } from 'lucide-react'; -import { parseAsIsoDateTime, useQueryState } from 'nuqs'; - -type Props = { - projectId: string; - profileId?: string; -}; - -const Events = ({ projectId, profileId }: Props) => { - const [filters] = useEventQueryFilters(); - const [startDate, setStartDate] = useQueryState( - 'startDate', - parseAsIsoDateTime, - ); - const [eventNames] = useEventQueryNamesFilter(); - - const [endDate, setEndDate] = useQueryState('endDate', parseAsIsoDateTime); - const query = api.event.events.useInfiniteQuery( - { - projectId, - filters, - events: eventNames, - profileId, - startDate: startDate || undefined, - endDate: endDate || undefined, - }, - { - getNextPageParam: (lastPage) => lastPage.meta.next, - keepPreviousData: true, - }, - ); - - return ( -
- - query.refetch()} /> - - - - - {query.isRefetching && ( -
- -
- )} -
- -
- ); -}; - -export default Events; diff --git a/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/events/page.tsx b/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/events/page.tsx deleted file mode 100644 index 64c590f8..00000000 --- a/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/events/page.tsx +++ /dev/null @@ -1,49 +0,0 @@ -import { PageTabs, PageTabsLink } from '@/components/page-tabs'; -import { Padding } from '@/components/ui/padding'; -import { parseAsStringEnum } from 'nuqs/server'; - -import Charts from './charts'; -import Conversions from './conversions'; -import Events from './events'; - -interface PageProps { - params: { - projectId: string; - }; - searchParams: Record; -} - -export default function Page({ - params: { projectId }, - searchParams, -}: PageProps) { - const tab = parseAsStringEnum(['events', 'conversions', 'charts']) - .withDefault('events') - .parseServerSide(searchParams.tab); - - return ( - <> - -
- - - Events - - - Conversions - - - Charts - - -
- {tab === 'events' && } - {tab === 'conversions' && } - {tab === 'charts' && } -
- - ); -} diff --git a/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/layout-content.tsx b/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/layout-content.tsx deleted file mode 100644 index 316392aa..00000000 --- a/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/layout-content.tsx +++ /dev/null @@ -1,33 +0,0 @@ -'use client'; - -import { cn } from '@/utils/cn'; -import { useSelectedLayoutSegments } from 'next/navigation'; - -const NOT_MIGRATED_PAGES = ['reports']; - -export default function LayoutContent({ - children, -}: { - children: React.ReactNode; -}) { - const segments = useSelectedLayoutSegments(); - - if (segments[0] && NOT_MIGRATED_PAGES.includes(segments[0])) { - return ( -
- {children} -
- ); - } - - return ( -
- {children} -
- ); -} diff --git a/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/layout-menu.tsx b/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/layout-menu.tsx deleted file mode 100644 index 8111744e..00000000 --- a/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/layout-menu.tsx +++ /dev/null @@ -1,243 +0,0 @@ -'use client'; - -import { Button } from '@/components/ui/button'; -import { pushModal } from '@/modals'; -import { cn } from '@/utils/cn'; -import { - BanknoteIcon, - ChartLineIcon, - DollarSignIcon, - GanttChartIcon, - Globe2Icon, - HeartHandshakeIcon, - LayersIcon, - LayoutPanelTopIcon, - PlusIcon, - ScanEyeIcon, - ServerIcon, - SparklesIcon, - UsersIcon, - WallpaperIcon, -} from 'lucide-react'; -import type { LucideIcon } from 'lucide-react'; -import { usePathname } from 'next/navigation'; - -import { ProjectLink } from '@/components/links'; -import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert'; -import { CommandShortcut } from '@/components/ui/command'; -import { useNumber } from '@/hooks/useNumerFormatter'; -import type { IServiceDashboards, IServiceOrganization } from '@openpanel/db'; -import { differenceInDays, format } from 'date-fns'; - -function LinkWithIcon({ - href, - icon: Icon, - label, - active: overrideActive, - className, -}: { - href: string; - icon: LucideIcon; - label: React.ReactNode; - active?: boolean; - className?: string; -}) { - const pathname = usePathname(); - const active = overrideActive || href === pathname; - return ( - - -
{label}
-
- ); -} - -interface LayoutMenuProps { - dashboards: IServiceDashboards; - organization: IServiceOrganization; -} -export default function LayoutMenu({ - dashboards, - organization, -}: LayoutMenuProps) { - const number = useNumber(); - const { - isTrial, - isExpired, - isExceeded, - isCanceled, - subscriptionEndsAt, - subscriptionPeriodEventsCount, - subscriptionPeriodEventsLimit, - subscriptionProductId, - } = organization; - return ( - <> -
- {(subscriptionProductId === '036efa2a-b3b4-4c75-b24a-9cac6bb8893b' || - subscriptionProductId === 'a18b4bee-d3db-4404-be6f-fba2f042d9ed') && ( - - -
-
Free plan is removed
-
- We've removed the free plan. You can upgrade to a paid plan to - continue using OpenPanel. -
-
-
- )} - {process.env.NEXT_PUBLIC_SELF_HOSTED === 'true' && ( - - -
-
Become a supporter
-
-
- )} - {isTrial && subscriptionEndsAt && ( - - -
-
- Free trial ends in{' '} - {differenceInDays(subscriptionEndsAt, new Date())} days -
-
-
- )} - {isExpired && subscriptionEndsAt && ( - - -
-
Subscription expired
-
- You can still use OpenPanel but you won't have access to new - incoming data. -
-
-
- )} - {isCanceled && subscriptionEndsAt && ( - - -
-
Subscription canceled
-
- {differenceInDays(new Date(), subscriptionEndsAt)} days ago -
-
-
- )} - {isExceeded && subscriptionEndsAt && ( - - -
-
Events limit exceeded
-
- {number.format(subscriptionPeriodEventsCount)} /{' '} - {number.format(subscriptionPeriodEventsLimit)} -
-
-
- )} - - -
-
Ask AI
-
- ⌘K -
- - -
-
Create report
-
- ⌘J -
-
- - - - - - - -
-
-
Your dashboards
- -
-
- {dashboards.map((item) => ( - - ))} -
-
- {process.env.NEXT_PUBLIC_SELF_HOSTED === 'true' && ( -
-
- Self-hosted instance -
-
- )} - - ); -} diff --git a/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/layout-organization-selector.tsx b/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/layout-organization-selector.tsx deleted file mode 100644 index 6eabdaba..00000000 --- a/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/layout-organization-selector.tsx +++ /dev/null @@ -1,43 +0,0 @@ -'use client'; - -import { Combobox } from '@/components/ui/combobox'; -import { useAppParams } from '@/hooks/useAppParams'; -import { Building } from 'lucide-react'; -import { useRouter } from 'next/navigation'; - -import type { IServiceOrganization } from '@openpanel/db'; - -interface LayoutOrganizationSelectorProps { - organizations: IServiceOrganization[]; -} - -export default function LayoutOrganizationSelector({ - organizations, -}: LayoutOrganizationSelectorProps) { - const params = useAppParams(); - const router = useRouter(); - - const organization = organizations.find( - (item) => item.id === params.organizationId, - ); - - return ( - item.id) - .map((item) => ({ - label: item.name, - value: item.id, - })) ?? [] - } - onChange={(value) => { - router.push(`/${value}`); - }} - /> - ); -} diff --git a/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/layout-sidebar.tsx b/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/layout-sidebar.tsx deleted file mode 100644 index afcc92a5..00000000 --- a/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/layout-sidebar.tsx +++ /dev/null @@ -1,90 +0,0 @@ -'use client'; - -import { LogoSquare } from '@/components/logo'; -import SettingsToggle from '@/components/settings-toggle'; -import { Button } from '@/components/ui/button'; -import { cn } from '@/utils/cn'; -import { MenuIcon, XIcon } from 'lucide-react'; -import { usePathname } from 'next/navigation'; -import { useEffect, useState } from 'react'; - -import type { - IServiceDashboards, - IServiceOrganization, - getProjectsByOrganizationId, -} from '@openpanel/db'; - -import { useAppParams } from '@/hooks/useAppParams'; -import Link from 'next/link'; -import LayoutMenu from './layout-menu'; -import LayoutProjectSelector from './layout-project-selector'; - -interface LayoutSidebarProps { - organizations: IServiceOrganization[]; - dashboards: IServiceDashboards; - projectId: string; - projects: Awaited>; -} -export function LayoutSidebar({ - organizations, - dashboards, - projects, -}: LayoutSidebarProps) { - const [active, setActive] = useState(false); - const pathname = usePathname(); - const { organizationId } = useAppParams(); - const organization = organizations.find((o) => o.id === organizationId)!; - - useEffect(() => { - setActive(false); - }, [pathname]); - - return ( - <> - - -
- - - - - -
-
- -
-
-
-
-
- - ); -} diff --git a/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/layout-sticky-below-header.tsx b/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/layout-sticky-below-header.tsx deleted file mode 100644 index 5c29c82d..00000000 --- a/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/layout-sticky-below-header.tsx +++ /dev/null @@ -1,22 +0,0 @@ -import { cn } from '@/utils/cn'; - -interface StickyBelowHeaderProps { - children: React.ReactNode; - className?: string; -} - -export function StickyBelowHeader({ - children, - className, -}: StickyBelowHeaderProps) { - return ( -
- {children} -
- ); -} diff --git a/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/layout.tsx b/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/layout.tsx deleted file mode 100644 index c4e14f6d..00000000 --- a/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/layout.tsx +++ /dev/null @@ -1,66 +0,0 @@ -import { FullPageEmptyState } from '@/components/full-page-empty-state'; - -import { - getDashboardsByProjectId, - getOrganizations, - getProjects, -} from '@openpanel/db'; - -import { auth } from '@openpanel/auth/nextjs'; -import LayoutContent from './layout-content'; -import { LayoutSidebar } from './layout-sidebar'; -import SideEffects from './side-effects'; - -interface AppLayoutProps { - children: React.ReactNode; - params: { - organizationSlug: string; - projectId: string; - }; -} - -export default async function AppLayout({ - children, - params: { organizationSlug: organizationId, projectId }, -}: AppLayoutProps) { - const { userId } = await auth(); - const [organizations, projects, dashboards] = await Promise.all([ - getOrganizations(userId), - getProjects({ organizationId, userId }), - getDashboardsByProjectId(projectId), - ]); - - const organization = organizations.find((item) => item.id === organizationId); - - if (!organization) { - return ( - - The organization you were looking for could not be found. - - ); - } - - if (!projects.find((item) => item.id === projectId)) { - return ( - - The project you were looking for could not be found. - - ); - } - - return ( -
- - {children} - -
- ); -} diff --git a/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/page-layout.tsx b/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/page-layout.tsx deleted file mode 100644 index 7589b4ef..00000000 --- a/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/page-layout.tsx +++ /dev/null @@ -1,15 +0,0 @@ -interface PageLayoutProps { - title: React.ReactNode; -} - -function PageLayout({ title }: PageLayoutProps) { - return ( - <> -
-
{title}
-
- - ); -} - -export default PageLayout; diff --git a/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/pages/page.tsx b/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/pages/page.tsx deleted file mode 100644 index 933771d0..00000000 --- a/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/pages/page.tsx +++ /dev/null @@ -1,34 +0,0 @@ -import { PageTabs, PageTabsLink } from '@/components/page-tabs'; -import { Padding } from '@/components/ui/padding'; -import { parseAsStringEnum } from 'nuqs/server'; - -import { Pages } from './pages'; - -interface PageProps { - params: { - projectId: string; - }; - searchParams: { - tab: string; - }; -} - -export default function Page({ - params: { projectId }, - searchParams, -}: PageProps) { - const tab = parseAsStringEnum(['pages', 'trends']) - .withDefault('pages') - .parseServerSide(searchParams.tab); - - return ( - - - - Pages - - - {tab === 'pages' && } - - ); -} diff --git a/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/pages/pages.tsx b/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/pages/pages.tsx deleted file mode 100644 index 3195af0f..00000000 --- a/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/pages/pages.tsx +++ /dev/null @@ -1,193 +0,0 @@ -'use client'; - -import { TableButtons } from '@/components/data-table'; -import { Input } from '@/components/ui/input'; -import { useDebounceValue } from '@/hooks/useDebounceValue'; -import { type RouterOutputs, api } from '@/trpc/client'; -import { parseAsInteger, useQueryState } from 'nuqs'; - -import { OverviewFiltersDrawer } from '@/components/overview/filters/overview-filters-drawer'; -import { OverviewInterval } from '@/components/overview/overview-interval'; -import { OverviewRange } from '@/components/overview/overview-range'; -import { useOverviewOptions } from '@/components/overview/useOverviewOptions'; -import { Pagination } from '@/components/pagination'; -import { ReportChart } from '@/components/report-chart'; -import { useEventQueryFilters } from '@/hooks/useEventQueryFilters'; -import { useNumber } from '@/hooks/useNumerFormatter'; -import type { IChartRange, IInterval } from '@openpanel/validation'; -import { memo } from 'react'; - -export function Pages({ projectId }: { projectId: string }) { - const take = 20; - const { range, interval } = useOverviewOptions(); - const [filters, setFilters] = useEventQueryFilters(); - const [cursor, setCursor] = useQueryState( - 'cursor', - parseAsInteger.withDefault(0), - ); - const [search, setSearch] = useQueryState('search', { - defaultValue: '', - shallow: true, - }); - const debouncedSearch = useDebounceValue(search, 500); - const query = api.event.pages.useQuery( - { - projectId, - cursor, - take, - search: debouncedSearch, - range, - interval, - filters, - }, - { - keepPreviousData: true, - }, - ); - const data = query.data ?? []; - - return ( - <> - - - - - { - setSearch(e.target.value); - setCursor(0); - }} - /> - -
- {data.map((page) => { - return ( - - ); - })} -
-
- -
- - ); -} - -const PageCard = memo( - ({ - page, - range, - interval, - projectId, - }: { - page: RouterOutputs['event']['pages'][number]; - range: IChartRange; - interval: IInterval; - projectId: string; - }) => { - const number = useNumber(); - return ( -
-
-
-
- {page.title} -
- - {page.path} - -
-
-
-
-
- {number.formatWithUnit(page.avg_duration, 'min')} -
-
- duration -
-
-
-
- {number.formatWithUnit(page.bounce_rate / 100, '%')} -
-
- bounce rate -
-
-
-
- {number.format(page.sessions)} -
-
- sessions -
-
-
- -
- ); - }, -); diff --git a/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/profiles/[profileId]/most-events/index.tsx b/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/profiles/[profileId]/most-events/index.tsx deleted file mode 100644 index 7220cdae..00000000 --- a/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/profiles/[profileId]/most-events/index.tsx +++ /dev/null @@ -1,20 +0,0 @@ -import withLoadingWidget from '@/hocs/with-loading-widget'; -import { escape } from 'sqlstring'; - -import { TABLE_NAMES, chQuery } from '@openpanel/db'; - -import MostEvents from './most-events'; - -type Props = { - projectId: string; - profileId: string; -}; - -const MostEventsServer = async ({ projectId, profileId }: Props) => { - const data = await chQuery<{ count: number; name: string }>( - `SELECT count(*) as count, name FROM ${TABLE_NAMES.events} WHERE name NOT IN ('screen_view', 'session_start', 'session_end') AND project_id = ${escape(projectId)} and profile_id = ${escape(profileId)} GROUP BY name ORDER BY count DESC`, - ); - return ; -}; - -export default withLoadingWidget(MostEventsServer); diff --git a/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/profiles/[profileId]/page.tsx b/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/profiles/[profileId]/page.tsx deleted file mode 100644 index 347296a4..00000000 --- a/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/profiles/[profileId]/page.tsx +++ /dev/null @@ -1,80 +0,0 @@ -import ClickToCopy from '@/components/click-to-copy'; -import { ProfileAvatar } from '@/components/profiles/profile-avatar'; -import { Padding } from '@/components/ui/padding'; -import { getProfileName } from '@/utils/getters'; -import { notFound } from 'next/navigation'; - -import { getProfileById, getProfileByIdCached } from '@openpanel/db'; - -import MostEventsServer from './most-events'; -import PopularRoutesServer from './popular-routes'; -import ProfileActivityServer from './profile-activity'; -import ProfileCharts from './profile-charts'; -import Events from './profile-events'; -import ProfileMetrics from './profile-metrics'; - -interface PageProps { - params: { - projectId: string; - profileId: string; - }; - searchParams: { - events?: string; - cursor?: string; - f?: string; - startDate: string; - endDate: string; - }; -} - -export default async function Page({ - params: { projectId, profileId }, -}: PageProps) { - const profile = await getProfileById( - decodeURIComponent(profileId), - projectId, - ); - - if (!profile) { - return notFound(); - } - - return ( - -
- -
- -

- {getProfileName(profile)} -

-
-
-
-
-
-
- -
-
- -
-
- -
-
- -
- - -
-
- -
-
-
- ); -} diff --git a/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/profiles/[profileId]/popular-routes/index.tsx b/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/profiles/[profileId]/popular-routes/index.tsx deleted file mode 100644 index c01eca22..00000000 --- a/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/profiles/[profileId]/popular-routes/index.tsx +++ /dev/null @@ -1,20 +0,0 @@ -import withLoadingWidget from '@/hocs/with-loading-widget'; -import { escape } from 'sqlstring'; - -import { TABLE_NAMES, chQuery } from '@openpanel/db'; - -import PopularRoutes from './popular-routes'; - -type Props = { - projectId: string; - profileId: string; -}; - -const PopularRoutesServer = async ({ projectId, profileId }: Props) => { - const data = await chQuery<{ count: number; path: string }>( - `SELECT count(*) as count, path FROM ${TABLE_NAMES.events} WHERE name = 'screen_view' AND project_id = ${escape(projectId)} and profile_id = ${escape(profileId)} GROUP BY path ORDER BY count DESC`, - ); - return ; -}; - -export default withLoadingWidget(PopularRoutesServer); diff --git a/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/profiles/[profileId]/profile-activity/index.tsx b/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/profiles/[profileId]/profile-activity/index.tsx deleted file mode 100644 index 61aa195f..00000000 --- a/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/profiles/[profileId]/profile-activity/index.tsx +++ /dev/null @@ -1,20 +0,0 @@ -import withLoadingWidget from '@/hocs/with-loading-widget'; -import { escape } from 'sqlstring'; - -import { TABLE_NAMES, chQuery } from '@openpanel/db'; - -import ProfileActivity from './profile-activity'; - -type Props = { - projectId: string; - profileId: string; -}; - -const ProfileActivityServer = async ({ projectId, profileId }: Props) => { - const data = await chQuery<{ count: number; date: string }>( - `SELECT count(*) as count, toStartOfDay(created_at) as date FROM ${TABLE_NAMES.events} WHERE project_id = ${escape(projectId)} and profile_id = ${escape(profileId)} GROUP BY date ORDER BY date DESC`, - ); - return ; -}; - -export default withLoadingWidget(ProfileActivityServer); diff --git a/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/profiles/[profileId]/profile-activity/profile-activity.tsx b/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/profiles/[profileId]/profile-activity/profile-activity.tsx deleted file mode 100644 index d6949c81..00000000 --- a/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/profiles/[profileId]/profile-activity/profile-activity.tsx +++ /dev/null @@ -1,164 +0,0 @@ -'use client'; - -import { Button } from '@/components/ui/button'; -import { - Widget, - WidgetBody, - WidgetHead, - WidgetTitle, -} from '@/components/widget'; -import { cn } from '@/utils/cn'; -import { - addMonths, - eachDayOfInterval, - endOfMonth, - format, - formatISO, - isSameMonth, - startOfMonth, - subMonths, -} from 'date-fns'; -import { ActivityIcon, ChevronLeftIcon, ChevronRightIcon } from 'lucide-react'; -import { useState } from 'react'; - -type Props = { - data: { count: number; date: string }[]; -}; - -const ProfileActivity = ({ data }: Props) => { - const [startDate, setStartDate] = useState(startOfMonth(new Date())); - const endDate = endOfMonth(startDate); - return ( - - - Activity -
- - - -
-
- -
-
-
- {format(subMonths(startDate, 3), 'MMMM yyyy')} -
-
- {eachDayOfInterval({ - start: startOfMonth(subMonths(startDate, 3)), - end: endOfMonth(subMonths(startDate, 3)), - }).map((date) => { - const hit = data.find((item) => - item.date.includes( - formatISO(date, { representation: 'date' }), - ), - ); - return ( -
- ); - })} -
-
-
-
- {format(subMonths(startDate, 2), 'MMMM yyyy')} -
-
- {eachDayOfInterval({ - start: startOfMonth(subMonths(startDate, 2)), - end: endOfMonth(subMonths(startDate, 2)), - }).map((date) => { - const hit = data.find((item) => - item.date.includes( - formatISO(date, { representation: 'date' }), - ), - ); - return ( -
- ); - })} -
-
-
-
- {format(subMonths(startDate, 1), 'MMMM yyyy')} -
-
- {eachDayOfInterval({ - start: startOfMonth(subMonths(startDate, 1)), - end: endOfMonth(subMonths(startDate, 1)), - }).map((date) => { - const hit = data.find((item) => - item.date.includes( - formatISO(date, { representation: 'date' }), - ), - ); - return ( -
- ); - })} -
-
-
-
{format(startDate, 'MMMM yyyy')}
-
- {eachDayOfInterval({ - start: startDate, - end: endDate, - }).map((date) => { - const hit = data.find((item) => - item.date.includes( - formatISO(date, { representation: 'date' }), - ), - ); - return ( -
- ); - })} -
-
-
- - - ); -}; - -export default ProfileActivity; diff --git a/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/profiles/[profileId]/profile-charts.tsx b/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/profiles/[profileId]/profile-charts.tsx deleted file mode 100644 index b293d674..00000000 --- a/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/profiles/[profileId]/profile-charts.tsx +++ /dev/null @@ -1,106 +0,0 @@ -'use client'; - -import { ReportChart } from '@/components/report-chart'; -import { Widget, WidgetBody, WidgetHead } from '@/components/widget'; -import { memo } from 'react'; - -import type { IChartProps } from '@openpanel/validation'; - -type Props = { - profileId: string; - projectId: string; -}; - -const ProfileCharts = ({ profileId, projectId }: Props) => { - const pageViewsChart: IChartProps = { - projectId, - chartType: 'linear', - events: [ - { - segment: 'event', - filters: [ - { - id: 'profile_id', - name: 'profile_id', - operator: 'is', - value: [profileId], - }, - ], - id: 'A', - name: 'screen_view', - displayName: 'Events', - }, - ], - breakdowns: [ - { - id: 'path', - name: 'path', - }, - ], - lineType: 'monotone', - interval: 'day', - name: 'Events', - range: '30d', - previous: false, - metric: 'sum', - }; - - const eventsChart: IChartProps = { - projectId, - chartType: 'linear', - events: [ - { - segment: 'event', - filters: [ - { - id: 'profile_id', - name: 'profile_id', - operator: 'is', - value: [profileId], - }, - ], - id: 'A', - name: '*', - displayName: 'Events', - }, - ], - breakdowns: [ - { - id: 'name', - name: 'name', - }, - ], - lineType: 'monotone', - interval: 'day', - name: 'Events', - range: '30d', - previous: false, - metric: 'sum', - }; - - return ( - <> - - - Page views - - - - - - - - Events per day - - - - - - - ); -}; - -// No clue why I need to check for equality here -export default memo(ProfileCharts, (a, b) => { - return a.profileId === b.profileId && a.projectId === b.projectId; -}); diff --git a/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/profiles/[profileId]/profile-events.tsx b/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/profiles/[profileId]/profile-events.tsx deleted file mode 100644 index 586527b8..00000000 --- a/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/profiles/[profileId]/profile-events.tsx +++ /dev/null @@ -1,55 +0,0 @@ -'use client'; - -import { TableButtons } from '@/components/data-table'; -import { EventsTable } from '@/components/events/table'; -import { EventsTableColumns } from '@/components/events/table/events-table-columns'; -import { OverviewFiltersButtons } from '@/components/overview/filters/overview-filters-buttons'; -import { OverviewFiltersDrawer } from '@/components/overview/filters/overview-filters-drawer'; -import { useEventQueryFilters } from '@/hooks/useEventQueryFilters'; -import { api } from '@/trpc/client'; -import { Loader2Icon } from 'lucide-react'; - -type Props = { - projectId: string; - profileId: string; -}; - -const Events = ({ projectId, profileId }: Props) => { - const [filters] = useEventQueryFilters(); - const query = api.event.events.useInfiniteQuery( - { - projectId, - filters, - profileId, - }, - { - getNextPageParam: (lastPage) => lastPage.meta.next, - keepPreviousData: true, - }, - ); - - return ( -
- - - - - {query.isRefetching && ( -
- -
- )} -
- -
- ); -}; - -export default Events; diff --git a/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/profiles/[profileId]/profile-metrics/index.tsx b/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/profiles/[profileId]/profile-metrics/index.tsx deleted file mode 100644 index 36900b68..00000000 --- a/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/profiles/[profileId]/profile-metrics/index.tsx +++ /dev/null @@ -1,18 +0,0 @@ -import withSuspense from '@/hocs/with-suspense'; - -import type { IServiceProfile } from '@openpanel/db'; -import { getProfileMetrics } from '@openpanel/db'; - -import ProfileMetrics from './profile-metrics'; - -type Props = { - projectId: string; - profile: IServiceProfile; -}; - -const ProfileMetricsServer = async ({ projectId, profile }: Props) => { - const data = await getProfileMetrics(profile.id, projectId); - return ; -}; - -export default withSuspense(ProfileMetricsServer, () => null); diff --git a/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/profiles/[profileId]/profile-metrics/profile-metrics.tsx b/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/profiles/[profileId]/profile-metrics/profile-metrics.tsx deleted file mode 100644 index 73ec37fd..00000000 --- a/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/profiles/[profileId]/profile-metrics/profile-metrics.tsx +++ /dev/null @@ -1,123 +0,0 @@ -'use client'; - -import { ListPropertiesIcon } from '@/components/events/list-properties-icon'; -import { useNumber } from '@/hooks/useNumerFormatter'; -import { cn } from '@/utils/cn'; -import { formatDateTime, utc } from '@/utils/date'; -import { formatDistanceToNow } from 'date-fns'; -import { parseAsStringEnum, useQueryState } from 'nuqs'; - -import type { IProfileMetrics, IServiceProfile } from '@openpanel/db'; - -type Props = { - data: IProfileMetrics; - profile: IServiceProfile; -}; - -function Card({ title, value }: { title: string; value: string }) { - return ( -
-
{title}
-
{value}
-
- ); -} - -function Info({ title, value }: { title: string; value: string }) { - return ( -
-
{title}
-
- {value - ? typeof value === 'string' - ? value - : JSON.stringify(value) - : '-'} -
-
- ); -} - -const ProfileMetrics = ({ data, profile }: Props) => { - const [tab, setTab] = useQueryState( - 'tab', - parseAsStringEnum(['profile', 'properties']).withDefault('profile'), - ); - const number = useNumber(); - return ( -
-
-
-
- -
- -
-
- {tab === 'profile' && ( - <> - - - - - - - - )} - {tab === 'properties' && - Object.entries(profile.properties) - .filter(([key, value]) => value !== undefined) - .map(([key, value]) => ( - - ))} -
-
- - - - - - -
-
- ); -}; - -export default ProfileMetrics; diff --git a/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/profiles/page.tsx b/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/profiles/page.tsx deleted file mode 100644 index 1f1bb38b..00000000 --- a/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/profiles/page.tsx +++ /dev/null @@ -1,44 +0,0 @@ -import { PageTabs, PageTabsLink } from '@/components/page-tabs'; -import { Padding } from '@/components/ui/padding'; -import { parseAsStringEnum } from 'nuqs/server'; - -import PowerUsers from './power-users'; -import Profiles from './profiles'; - -interface PageProps { - params: { - projectId: string; - }; - searchParams: Record; -} - -export default function Page({ - params: { projectId }, - searchParams, -}: PageProps) { - const tab = parseAsStringEnum(['profiles', 'power-users']) - .withDefault('profiles') - .parseServerSide(searchParams.tab); - - return ( - <> - -
- - - Profiles - - - Power users - - -
- {tab === 'profiles' && } - {tab === 'power-users' && } -
- - ); -} diff --git a/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/profiles/power-users.tsx b/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/profiles/power-users.tsx deleted file mode 100644 index e4da65eb..00000000 --- a/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/profiles/power-users.tsx +++ /dev/null @@ -1,41 +0,0 @@ -'use client'; - -import { ProfilesTable } from '@/components/profiles/table'; -import { api } from '@/trpc/client'; -import { parseAsInteger, useQueryState } from 'nuqs'; - -type Props = { - projectId: string; - profileId?: string; -}; - -const Events = ({ projectId }: Props) => { - const [cursor, setCursor] = useQueryState( - 'cursor', - parseAsInteger.withDefault(0), - ); - const query = api.profile.powerUsers.useQuery( - { - cursor, - projectId, - take: 50, - // filters, - }, - { - keepPreviousData: true, - }, - ); - - return ( -
- -
- ); -}; - -export default Events; diff --git a/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/profiles/profile-last-seen/index.tsx b/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/profiles/profile-last-seen/index.tsx deleted file mode 100644 index 2f74747f..00000000 --- a/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/profiles/profile-last-seen/index.tsx +++ /dev/null @@ -1,68 +0,0 @@ -import { - Tooltip, - TooltipContent, - TooltipTrigger, -} from '@/components/ui/tooltip'; -import { Widget, WidgetBody, WidgetHead } from '@/components/widget'; -import { cn } from '@/utils/cn'; -import { escape } from 'sqlstring'; - -import { TABLE_NAMES, chQuery } from '@openpanel/db'; - -interface Props { - projectId: string; -} - -export default async function ProfileLastSeenServer({ projectId }: Props) { - interface Row { - days: number; - count: number; - } - // Days since last event from users - // group by days - const res = await chQuery( - `SELECT age('days',created_at, now()) as days, count(distinct profile_id) as count FROM ${TABLE_NAMES.events} where project_id = ${escape(projectId)} group by days order by days ASC LIMIT 51`, - ); - - const maxValue = Math.max(...res.map((x) => x.count)); - const minValue = Math.min(...res.map((x) => x.count)); - const calculateRatio = (currentValue: number) => - Math.max( - 0.1, - Math.min(1, (currentValue - minValue) / (maxValue - minValue)), - ); - - const renderItem = (item: Row) => ( -
- - -
- - - {item.count} profiles last seen{' '} - {item.days === 0 ? 'today' : `${item.days} days ago`} - - -
{item.days}
-
- ); - - return ( - - -
Last seen
-
- -
- {res.map(renderItem)} -
-
DAYS
-
-
- ); -} diff --git a/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/profiles/profiles.tsx b/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/profiles/profiles.tsx deleted file mode 100644 index e751ae26..00000000 --- a/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/profiles/profiles.tsx +++ /dev/null @@ -1,51 +0,0 @@ -'use client'; - -import { TableButtons } from '@/components/data-table'; -import { ProfilesTable } from '@/components/profiles/table'; -import { Input } from '@/components/ui/input'; -import { useDebounceValue } from '@/hooks/useDebounceValue'; -import { api } from '@/trpc/client'; -import { parseAsInteger, useQueryState } from 'nuqs'; - -type Props = { - projectId: string; - profileId?: string; -}; - -const Events = ({ projectId }: Props) => { - const [cursor, setCursor] = useQueryState( - 'cursor', - parseAsInteger.withDefault(0), - ); - const [search, setSearch] = useQueryState('search', { - defaultValue: '', - shallow: true, - }); - const debouncedSearch = useDebounceValue(search, 500); - const query = api.profile.list.useQuery( - { - cursor, - projectId, - take: 50, - search: debouncedSearch, - }, - { - keepPreviousData: true, - }, - ); - - return ( -
- - setSearch(e.target.value)} - placeholder="Search profiles" - /> - - -
- ); -}; - -export default Events; diff --git a/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/realtime/map/coordinates.ts b/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/realtime/map/coordinates.ts deleted file mode 100644 index 7e430cae..00000000 --- a/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/realtime/map/coordinates.ts +++ /dev/null @@ -1,222 +0,0 @@ -export type Coordinate = { - lat: number; - long: number; -}; - -export function haversineDistance( - coord1: Coordinate, - coord2: Coordinate, -): number { - const R = 6371; // Earth's radius in kilometers - const lat1Rad = coord1.lat * (Math.PI / 180); - const lat2Rad = coord2.lat * (Math.PI / 180); - const deltaLatRad = (coord2.lat - coord1.lat) * (Math.PI / 180); - const deltaLonRad = (coord2.long - coord1.long) * (Math.PI / 180); - - const a = - Math.sin(deltaLatRad / 2) * Math.sin(deltaLatRad / 2) + - Math.cos(lat1Rad) * - Math.cos(lat2Rad) * - Math.sin(deltaLonRad / 2) * - Math.sin(deltaLonRad / 2); - const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a)); - - return R * c; // Distance in kilometers -} - -export function findFarthestPoints( - coordinates: Coordinate[], -): [Coordinate, Coordinate] { - if (coordinates.length < 2) { - throw new Error('At least two coordinates are required'); - } - - let maxDistance = 0; - let point1: Coordinate = coordinates[0]!; - let point2: Coordinate = coordinates[1]!; - - for (let i = 0; i < coordinates.length; i++) { - for (let j = i + 1; j < coordinates.length; j++) { - const distance = haversineDistance(coordinates[i]!, coordinates[j]!); - if (distance > maxDistance) { - maxDistance = distance; - point1 = coordinates[i]!; - point2 = coordinates[j]!; - } - } - } - - return [point1, point2]; -} - -export function getAverageCenter(coordinates: Coordinate[]): Coordinate { - if (coordinates.length === 0) { - return { long: 0, lat: 20 }; - } - - let sumLong = 0; - let sumLat = 0; - - for (const coord of coordinates) { - sumLong += coord.long; - sumLat += coord.lat; - } - - const avgLat = sumLat / coordinates.length; - const avgLong = sumLong / coordinates.length; - - return { long: avgLong, lat: avgLat }; -} - -function sortCoordinates(a: Coordinate, b: Coordinate): number { - return a.long === b.long ? a.lat - b.lat : a.long - b.long; -} - -function cross(o: Coordinate, a: Coordinate, b: Coordinate): number { - return ( - (a.long - o.long) * (b.lat - o.lat) - (a.lat - o.lat) * (b.long - o.long) - ); -} - -// convex hull -export function getOuterMarkers(coordinates: Coordinate[]): Coordinate[] { - const sorted = coordinates.sort(sortCoordinates); - - if (sorted.length <= 3) return sorted; - - const lower: Coordinate[] = []; - for (const coord of sorted) { - while ( - lower.length >= 2 && - cross(lower[lower.length - 2]!, lower[lower.length - 1]!, coord) <= 0 - ) { - lower.pop(); - } - lower.push(coord); - } - - const upper: Coordinate[] = []; - for (let i = coordinates.length - 1; i >= 0; i--) { - while ( - upper.length >= 2 && - cross(upper[upper.length - 2]!, upper[upper.length - 1]!, sorted[i]!) <= 0 - ) { - upper.pop(); - } - upper.push(sorted[i]!); - } - - upper.pop(); - lower.pop(); - return lower.concat(upper); -} - -export function calculateCentroid(polygon: Coordinate[]): Coordinate { - if (polygon.length < 3) { - throw new Error('At least three points are required to form a polygon.'); - } - - let area = 0; - let centroidLat = 0; - let centroidLong = 0; - - for (let i = 0, j = polygon.length - 1; i < polygon.length; j = i++) { - const x0 = polygon[j]!.long; - const y0 = polygon[j]!.lat; - const x1 = polygon[i]!.long; - const y1 = polygon[i]!.lat; - const a = x0 * y1 - x1 * y0; - area += a; - centroidLong += (x0 + x1) * a; - centroidLat += (y0 + y1) * a; - } - - area = area / 2; - if (area === 0) { - // This should not happen for a proper convex hull - throw new Error('Area of the polygon is zero, check the coordinates.'); - } - - centroidLat /= 6 * area; - centroidLong /= 6 * area; - - return { lat: centroidLat, long: centroidLong }; -} - -export function calculateGeographicMidpoint( - coordinate: Coordinate[], -): Coordinate { - let minLat = Number.POSITIVE_INFINITY; - let maxLat = Number.NEGATIVE_INFINITY; - let minLong = Number.POSITIVE_INFINITY; - let maxLong = Number.NEGATIVE_INFINITY; - - for (const { lat, long } of coordinate) { - if (lat < minLat) minLat = lat; - if (lat > maxLat) maxLat = lat; - if (long < minLong) minLong = long; - if (long > maxLong) maxLong = long; - } - - // Handling the wrap around the international date line - let midLong: number; - if (maxLong > minLong) { - midLong = (maxLong + minLong) / 2; - } else { - // Adjust calculation when spanning the dateline - midLong = ((maxLong + 360 + minLong) / 2) % 360; - } - - const midLat = (maxLat + minLat) / 2; - - return { lat: midLat, long: midLong }; -} - -export function clusterCoordinates(coordinates: Coordinate[], radius = 25) { - const clusters: { - center: Coordinate; - count: number; - members: Coordinate[]; - }[] = []; - const visited = new Set(); - - coordinates.forEach((coord, idx) => { - if (!visited.has(idx)) { - const cluster = { - members: [coord], - center: { lat: coord.lat, long: coord.long }, - count: 0, - }; - - coordinates.forEach((otherCoord, otherIdx) => { - if ( - !visited.has(otherIdx) && - haversineDistance(coord, otherCoord) <= radius - ) { - cluster.members.push(otherCoord); - visited.add(otherIdx); - cluster.count++; - } - }); - - // Calculate geographic center for the cluster - cluster.center = cluster.members.reduce( - (center, cur) => { - return { - lat: center.lat + cur.lat / cluster.members.length, - long: center.long + cur.long / cluster.members.length, - }; - }, - { lat: 0, long: 0 }, - ); - - clusters.push(cluster); - } - }); - - return clusters.map((cluster) => ({ - center: cluster.center, - count: cluster.count, - members: cluster.members, - })); -} diff --git a/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/realtime/map/index.tsx b/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/realtime/map/index.tsx deleted file mode 100644 index 59700e8a..00000000 --- a/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/realtime/map/index.tsx +++ /dev/null @@ -1,20 +0,0 @@ -import { subMinutes } from 'date-fns'; -import { escape } from 'sqlstring'; - -import { TABLE_NAMES, chQuery, formatClickhouseDate } from '@openpanel/db'; - -import type { Coordinate } from './coordinates'; -import Map from './map'; - -type Props = { - projectId: string; -}; -const RealtimeMap = async ({ projectId }: Props) => { - const res = await chQuery( - `SELECT DISTINCT city, longitude as long, latitude as lat FROM ${TABLE_NAMES.events} WHERE project_id = ${escape(projectId)} AND created_at >= '${formatClickhouseDate(subMinutes(new Date(), 30))}' ORDER BY created_at DESC`, - ); - - return ; -}; - -export default RealtimeMap; diff --git a/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/realtime/map/map.tsx b/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/realtime/map/map.tsx deleted file mode 100644 index 793031da..00000000 --- a/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/realtime/map/map.tsx +++ /dev/null @@ -1,185 +0,0 @@ -'use client'; - -import { useFullscreen } from '@/components/fullscreen-toggle'; -import { Tooltiper } from '@/components/ui/tooltip'; -import { cn } from '@/utils/cn'; -import { bind } from 'bind-event-listener'; -import { useTheme } from 'next-themes'; -import { Fragment, useEffect, useRef, useState } from 'react'; -import { - ComposableMap, - Geographies, - Geography, - Marker, -} from 'react-simple-maps'; - -import type { Coordinate } from './coordinates'; -import { - calculateGeographicMidpoint, - clusterCoordinates, - getAverageCenter, - getOuterMarkers, -} from './coordinates'; -import { - CustomZoomableGroup, - GEO_MAP_URL, - determineZoom, - getBoundingBox, - useAnimatedState, -} from './map.helpers'; -import { calculateMarkerSize } from './markers'; - -type Props = { - markers: Coordinate[]; -}; -const Map = ({ markers }: Props) => { - const [isFullscreen] = useFullscreen(); - const showCenterMarker = false; - const ref = useRef(null); - const [size, setSize] = useState<{ width: number; height: number } | null>( - null, - ); - - // const { markers, toggle } = useActiveMarkers(_m); - const hull = getOuterMarkers(markers); - const center = - hull.length < 2 - ? getAverageCenter(markers) - : calculateGeographicMidpoint(hull); - const boundingBox = getBoundingBox(hull); - const [zoom] = useAnimatedState( - markers.length === 1 - ? 1 - : determineZoom(boundingBox, size ? size?.height / size?.width : 1), - ); - - const [long] = useAnimatedState(center.long); - const [lat] = useAnimatedState(center.lat); - - useEffect(() => { - return bind(window, { - type: 'resize', - listener() { - if (ref.current) { - setSize({ - width: ref.current.clientWidth, - height: ref.current.clientHeight, - }); - } - }, - }); - }, []); - - useEffect(() => { - if (ref.current) { - setSize({ - width: ref.current.clientWidth, - height: ref.current.clientHeight, - }); - } - }, []); - - const adjustSizeBasedOnZoom = (size: number) => { - const minMultiplier = 1; - const maxMultiplier = 7; - - // Linearly interpolate the multiplier based on the zoom level - const multiplier = - maxMultiplier - ((zoom - 1) * (maxMultiplier - minMultiplier)) / (20 - 1); - - return size * multiplier; - }; - - const theme = useTheme(); - - return ( -
- {size === null ? ( - <> - ) : ( - <> - - - - {({ geographies }) => - geographies.map((geo) => ( - - )) - } - - {showCenterMarker && ( - - - - )} - {clusterCoordinates(markers).map((marker) => { - const size = adjustSizeBasedOnZoom( - calculateMarkerSize(marker.count), - ); - const coordinates: [number, number] = [ - marker.center.long, - marker.center.lat, - ]; - return ( - - - - - - - - - - - ); - })} - - - - )} - {/* */} -
- ); -}; - -export default Map; diff --git a/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/realtime/page.tsx b/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/realtime/page.tsx deleted file mode 100644 index 12e6485f..00000000 --- a/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/realtime/page.tsx +++ /dev/null @@ -1,145 +0,0 @@ -import { - Fullscreen, - FullscreenClose, - FullscreenOpen, -} from '@/components/fullscreen-toggle'; -import { ReportChart } from '@/components/report-chart'; -import { Suspense } from 'react'; - -import RealtimeMap from './map'; -import RealtimeLiveEventsServer from './realtime-live-events'; -import { RealtimeLiveHistogram } from './realtime-live-histogram'; -import RealtimeReloader from './realtime-reloader'; - -type Props = { - params: { - projectId: string; - }; -}; -export default function Page({ params: { projectId } }: Props) { - return ( - <> - - - - - - - -
- -
- -
-
- -
-
-
-
-
-
Pages
-
- -
-
-
-
Cities
-
- -
-
-
-
Referrers
-
- -
-
-
- - ); -} diff --git a/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/realtime/realtime-live-events/index.tsx b/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/realtime/realtime-live-events/index.tsx deleted file mode 100644 index 6fc9523f..00000000 --- a/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/realtime/realtime-live-events/index.tsx +++ /dev/null @@ -1,21 +0,0 @@ -import { escape } from 'sqlstring'; - -import { TABLE_NAMES, getEvents } from '@openpanel/db'; - -import LiveEvents from './live-events'; - -type Props = { - projectId: string; - limit?: number; -}; -const RealtimeLiveEventsServer = async ({ projectId, limit = 30 }: Props) => { - const events = await getEvents( - `SELECT * FROM ${TABLE_NAMES.events} WHERE created_at > now() - INTERVAL 2 HOUR AND project_id = ${escape(projectId)} ORDER BY created_at DESC LIMIT ${limit}`, - { - profile: true, - }, - ); - return ; -}; - -export default RealtimeLiveEventsServer; diff --git a/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/realtime/realtime-live-events/live-events.tsx b/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/realtime/realtime-live-events/live-events.tsx deleted file mode 100644 index 75cc06e6..00000000 --- a/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/realtime/realtime-live-events/live-events.tsx +++ /dev/null @@ -1,46 +0,0 @@ -'use client'; - -import { EventListItem } from '@/components/events/event-list-item'; -import useWS from '@/hooks/useWS'; -import { AnimatePresence, motion } from 'framer-motion'; -import { useState } from 'react'; - -import type { IServiceEvent, IServiceEventMinimal } from '@openpanel/db'; - -type Props = { - events: (IServiceEventMinimal | IServiceEvent)[]; - projectId: string; - limit: number; -}; - -const RealtimeLiveEvents = ({ events, projectId, limit }: Props) => { - const [state, setState] = useState(events ?? []); - useWS( - `/live/events/${projectId}`, - (event) => { - setState((p) => [event, ...p].slice(0, limit)); - }, - ); - return ( - -
- {state.map((event) => ( - -
- -
-
- ))} -
-
- ); -}; - -export default RealtimeLiveEvents; diff --git a/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/realtime/realtime-reloader.tsx b/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/realtime/realtime-reloader.tsx deleted file mode 100644 index 688d9d9c..00000000 --- a/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/realtime/realtime-reloader.tsx +++ /dev/null @@ -1,35 +0,0 @@ -'use client'; - -import useWS from '@/hooks/useWS'; -import { useQueryClient } from '@tanstack/react-query'; -import { useRouter } from 'next/navigation'; - -type Props = { - projectId: string; -}; - -const RealtimeReloader = ({ projectId }: Props) => { - const client = useQueryClient(); - const router = useRouter(); - - useWS( - `/live/events/${projectId}`, - () => { - if (!document.hidden) { - client.refetchQueries({ - type: 'active', - }); - } - }, - { - debounce: { - maxWait: 60000, - delay: 60000, - }, - }, - ); - - return null; -}; - -export default RealtimeReloader; diff --git a/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/reports/[reportId]/page.tsx b/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/reports/[reportId]/page.tsx deleted file mode 100644 index 4e9a7bc3..00000000 --- a/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/reports/[reportId]/page.tsx +++ /dev/null @@ -1,29 +0,0 @@ -import PageLayout from '@/app/(app)/[organizationSlug]/[projectId]/page-layout'; -import EditReportName from '@/components/report/edit-report-name'; -import { notFound } from 'next/navigation'; - -import { getReportById } from '@openpanel/db'; - -import ReportEditor from '../report-editor'; - -interface PageProps { - params: { - projectId: string; - reportId: string; - }; -} - -export default async function Page({ params: { reportId } }: PageProps) { - const report = await getReportById(reportId); - - if (!report) { - return notFound(); - } - - return ( - <> - } /> - - - ); -} diff --git a/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/reports/page.tsx b/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/reports/page.tsx deleted file mode 100644 index 9ac0223e..00000000 --- a/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/reports/page.tsx +++ /dev/null @@ -1,13 +0,0 @@ -import PageLayout from '@/app/(app)/[organizationSlug]/[projectId]/page-layout'; -import EditReportName from '@/components/report/edit-report-name'; - -import ReportEditor from './report-editor'; - -export default function Page() { - return ( - <> - } /> - - - ); -} diff --git a/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/retention/last-active-users/chart.tsx b/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/retention/last-active-users/chart.tsx deleted file mode 100644 index ca1659bb..00000000 --- a/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/retention/last-active-users/chart.tsx +++ /dev/null @@ -1,103 +0,0 @@ -'use client'; - -import { - useXAxisProps, - useYAxisProps, -} from '@/components/report-chart/common/axis'; -import { getChartColor } from '@/utils/theme'; -import { - Area, - AreaChart, - Tooltip as RechartTooltip, - ResponsiveContainer, - XAxis, - YAxis, -} from 'recharts'; - -type Props = { - data: { users: number; days: number }[]; -}; - -function Tooltip(props: any) { - const payload = props.payload?.[0]?.payload; - - if (!payload) { - return null; - } - return ( -
-
-
- Days since last seen -
-
{payload.days}
-
-
-
Active users
-
{payload.users}
-
-
- ); -} - -const Chart = ({ data }: Props) => { - const xAxisProps = useXAxisProps(); - const yAxisProps = useYAxisProps(); - return ( -
- - - - - - - - - - } /> - - - - - - -
- ); -}; - -export default Chart; diff --git a/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/retention/last-active-users/index.tsx b/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/retention/last-active-users/index.tsx deleted file mode 100644 index b654d370..00000000 --- a/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/retention/last-active-users/index.tsx +++ /dev/null @@ -1,25 +0,0 @@ -import { Widget, WidgetHead } from '@/components/widget'; -import withLoadingWidget from '@/hocs/with-loading-widget'; - -import { getRetentionLastSeenSeries } from '@openpanel/db'; - -import Chart from './chart'; - -type Props = { - projectId: string; -}; - -const LastActiveUsersServer = async ({ projectId }: Props) => { - const res = await getRetentionLastSeenSeries({ projectId }); - - return ( - - - Last time in days a user was active - - - - ); -}; - -export default withLoadingWidget(LastActiveUsersServer); diff --git a/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/retention/page.tsx b/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/retention/page.tsx deleted file mode 100644 index 39544215..00000000 --- a/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/retention/page.tsx +++ /dev/null @@ -1,65 +0,0 @@ -import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert'; -import { Padding } from '@/components/ui/padding'; -import { AlertCircleIcon } from 'lucide-react'; - -import LastActiveUsersServer from './last-active-users'; -import RollingActiveUsers from './rolling-active-users'; -import UsersRetentionSeries from './users-retention-series'; -import WeeklyCohortsServer from './weekly-cohorts'; - -type Props = { - params: { - projectId: string; - }; -}; - -const Retention = ({ params: { projectId } }: Props) => { - return ( - -

Retention

-
- - - Experimental feature - -

- This page is an experimental feature and we'll be working - hard to make it even better. Stay tuned! -

-

- Please DM me on{' '} - - Discord - {' '} - or{' '} - - X/Twitter - {' '} - if you notice any issues. -

-
-
- - - - Retention info - - This information is only relevant if you supply a user ID to the - SDK! - - - - {/* */} - -
-
- ); -}; - -export default Retention; diff --git a/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/retention/rolling-active-users/chart.tsx b/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/retention/rolling-active-users/chart.tsx deleted file mode 100644 index 6b01d806..00000000 --- a/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/retention/rolling-active-users/chart.tsx +++ /dev/null @@ -1,146 +0,0 @@ -'use client'; - -import { - useXAxisProps, - useYAxisProps, -} from '@/components/report-chart/common/axis'; -import { getChartColor } from '@/utils/theme'; -import { - Area, - AreaChart, - Tooltip as RechartTooltip, - ResponsiveContainer, - XAxis, - YAxis, -} from 'recharts'; - -import type { IServiceRetentionRollingActiveUsers } from '@openpanel/db'; - -type Props = { - data: { - daily: IServiceRetentionRollingActiveUsers[]; - weekly: IServiceRetentionRollingActiveUsers[]; - monthly: IServiceRetentionRollingActiveUsers[]; - }; -}; - -function Tooltip(props: any) { - const payload = props.payload?.[2]?.payload; - - if (!payload) { - return null; - } - return ( -
-
{payload.date}
-
-
- Monthly active users -
-
{payload.mau}
-
-
-
Weekly active users
-
{payload.wau}
-
-
-
Daily active users
-
{payload.dau}
-
-
- ); -} - -const Chart = ({ data }: Props) => { - const rechartData = data.daily.map((d) => ({ - date: new Date(d.date).getTime(), - dau: d.users, - wau: data.weekly.find((w) => w.date === d.date)?.users, - mau: data.monthly.find((m) => m.date === d.date)?.users, - })); - const xAxisProps = useXAxisProps({ interval: 'day' }); - const yAxisProps = useYAxisProps(); - return ( -
- - - - - - - - - - - - - - - - - - } /> - - - - - - - - -
- ); -}; - -export default Chart; diff --git a/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/retention/rolling-active-users/index.tsx b/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/retention/rolling-active-users/index.tsx deleted file mode 100644 index f0e1db08..00000000 --- a/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/retention/rolling-active-users/index.tsx +++ /dev/null @@ -1,35 +0,0 @@ -import { Widget, WidgetHead } from '@/components/widget'; -import withLoadingWidget from '@/hocs/with-loading-widget'; - -import { getRollingActiveUsers } from '@openpanel/db'; - -import Chart from './chart'; - -type Props = { - projectId: string; -}; - -const RollingActiveUsersServer = async ({ projectId }: Props) => { - const series = await Promise.all([ - await getRollingActiveUsers({ projectId, days: 1 }), - await getRollingActiveUsers({ projectId, days: 7 }), - await getRollingActiveUsers({ projectId, days: 30 }), - ]); - - return ( - - - Rolling active users - - - - ); -}; - -export default withLoadingWidget(RollingActiveUsersServer); diff --git a/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/retention/users-retention-series/chart.tsx b/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/retention/users-retention-series/chart.tsx deleted file mode 100644 index 8ecd28e6..00000000 --- a/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/retention/users-retention-series/chart.tsx +++ /dev/null @@ -1,117 +0,0 @@ -'use client'; - -import { - useXAxisProps, - useYAxisProps, -} from '@/components/report-chart/common/axis'; -import { useFormatDateInterval } from '@/hooks/useFormatDateInterval'; -import { formatDate } from '@/utils/date'; -import { getChartColor } from '@/utils/theme'; -import { - Area, - AreaChart, - Tooltip as RechartTooltip, - ResponsiveContainer, - XAxis, - YAxis, -} from 'recharts'; - -import { round } from '@openpanel/common'; - -type Props = { - data: { - date: string; - active_users: number; - retained_users: number; - retention: number; - }[]; -}; - -function Tooltip({ payload }: any) { - const { date, active_users, retained_users, retention } = - payload?.[0]?.payload || {}; - const formatDate = useFormatDateInterval('day'); - if (!date) { - return null; - } - return ( -
-
-
{formatDate(new Date(date))}
-
-
-
Active Users
-
{active_users}
-
-
-
Retained Users
-
{retained_users}
-
-
-
Retention
-
{round(retention, 2)}%
-
-
- ); -} - -const Chart = ({ data }: Props) => { - const xAxisProps = useXAxisProps(); - const yAxisProps = useYAxisProps(); - return ( -
- - - - - - - - - - } /> - - - formatDate(new Date(m))} - allowDuplicatedCategory={false} - label={{ - value: 'DATE', - position: 'insideBottom', - offset: 0, - fontSize: 10, - }} - /> - - - -
- ); -}; - -export default Chart; diff --git a/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/retention/users-retention-series/index.tsx b/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/retention/users-retention-series/index.tsx deleted file mode 100644 index 9a87469e..00000000 --- a/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/retention/users-retention-series/index.tsx +++ /dev/null @@ -1,25 +0,0 @@ -import { Widget, WidgetHead } from '@/components/widget'; -import withLoadingWidget from '@/hocs/with-loading-widget'; - -import { getRetentionSeries } from '@openpanel/db'; - -import Chart from './chart'; - -type Props = { - projectId: string; -}; - -const UsersRetentionSeries = async ({ projectId }: Props) => { - const res = await getRetentionSeries({ projectId }); - - return ( - - - Stickyness / Retention (%) - - - - ); -}; - -export default withLoadingWidget(UsersRetentionSeries); diff --git a/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/retention/weekly-cohorts/index.tsx b/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/retention/weekly-cohorts/index.tsx deleted file mode 100644 index db4c7a54..00000000 --- a/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/retention/weekly-cohorts/index.tsx +++ /dev/null @@ -1,132 +0,0 @@ -import { Widget, WidgetHead } from '@/components/widget'; -import { WidgetTableHead } from '@/components/widget-table'; -import withLoadingWidget from '@/hocs/with-loading-widget'; -import { cn } from '@/utils/cn'; - -import { getRetentionCohortTable } from '@openpanel/db'; - -type Props = { - projectId: string; -}; - -const Cell = ({ value, ratio }: { value: number; ratio: number }) => { - return ( - -
-
{value}
- - ); -}; - -const WeeklyCohortsServer = async ({ projectId }: Props) => { - const res = await getRetentionCohortTable({ projectId }); - - const minValue = 0; - const maxValue = Math.max( - ...res.flatMap((row) => [ - row.period_0, - row.period_1, - row.period_2, - row.period_3, - row.period_4, - row.period_5, - row.period_6, - row.period_7, - row.period_8, - row.period_9, - ]), - ); - - const calculateRatio = (currentValue: number) => - currentValue === 0 - ? 0 - : Math.max( - 0.1, - Math.min(1, (currentValue - minValue) / (maxValue - minValue)), - ); - - return ( - - - Weekly Cohorts - -
-
- - - - - - - - - - - - - - - - - - {res.map((row) => ( - - - - - - - - - - - - - - ))} - -
Week0123456789
- {row.first_seen} -
-
-
-
- ); -}; - -export default withLoadingWidget(WeeklyCohortsServer); diff --git a/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/settings/integrations/page.tsx b/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/settings/integrations/page.tsx deleted file mode 100644 index 53851847..00000000 --- a/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/settings/integrations/page.tsx +++ /dev/null @@ -1,36 +0,0 @@ -import { ActiveIntegrations } from '@/components/integrations/active-integrations'; -import { AllIntegrations } from '@/components/integrations/all-integrations'; -import { PageTabs, PageTabsLink } from '@/components/page-tabs'; -import { Padding } from '@/components/ui/padding'; -import { parseAsStringEnum } from 'nuqs/server'; - -interface PageProps { - params: { - projectId: string; - }; - searchParams: { - tab: string; - }; -} - -export default function Page({ - params: { projectId }, - searchParams, -}: PageProps) { - const tab = parseAsStringEnum(['installed', 'available']) - .withDefault('available') - .parseServerSide(searchParams.tab); - return ( - -
-

Your integrations

- -
- -
-

Available integrations

- -
-
- ); -} diff --git a/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/settings/loading.tsx b/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/settings/loading.tsx deleted file mode 100644 index 9cf2cf50..00000000 --- a/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/settings/loading.tsx +++ /dev/null @@ -1,3 +0,0 @@ -import FullPageLoadingState from '@/components/full-page-loading-state'; - -export default FullPageLoadingState; diff --git a/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/settings/notifications/page.tsx b/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/settings/notifications/page.tsx deleted file mode 100644 index 93d20930..00000000 --- a/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/settings/notifications/page.tsx +++ /dev/null @@ -1,40 +0,0 @@ -import { NotificationRules } from '@/components/notifications/notification-rules'; -import { Notifications } from '@/components/notifications/notifications'; -import { PageTabs, PageTabsLink } from '@/components/page-tabs'; -import { Padding } from '@/components/ui/padding'; -import { parseAsStringEnum } from 'nuqs/server'; - -interface PageProps { - params: { - projectId: string; - }; - searchParams: { - tab: string; - }; -} - -export default function Page({ - params: { projectId }, - searchParams, -}: PageProps) { - const tab = parseAsStringEnum(['notifications', 'rules']) - .withDefault('notifications') - .parseServerSide(searchParams.tab); - return ( - - - - Notifications - - - Rules - - - {tab === 'notifications' && } - {tab === 'rules' && } - - ); -} diff --git a/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/settings/organization/invites/index.tsx b/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/settings/organization/invites/index.tsx deleted file mode 100644 index 9484d973..00000000 --- a/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/settings/organization/invites/index.tsx +++ /dev/null @@ -1,28 +0,0 @@ -import { TableButtons } from '@/components/data-table'; -import { InvitesTable } from '@/components/settings/invites'; - -import { getInvites, getProjectsByOrganizationId } from '@openpanel/db'; - -import CreateInvite from './create-invite'; - -interface Props { - organizationId: string; -} - -const InvitesServer = async ({ organizationId }: Props) => { - const [invites, projects] = await Promise.all([ - getInvites(organizationId), - getProjectsByOrganizationId(organizationId), - ]); - - return ( -
- - - - -
- ); -}; - -export default InvitesServer; diff --git a/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/settings/organization/loading.tsx b/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/settings/organization/loading.tsx deleted file mode 100644 index 9cf2cf50..00000000 --- a/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/settings/organization/loading.tsx +++ /dev/null @@ -1,3 +0,0 @@ -import FullPageLoadingState from '@/components/full-page-loading-state'; - -export default FullPageLoadingState; diff --git a/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/settings/organization/members/index.tsx b/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/settings/organization/members/index.tsx deleted file mode 100644 index 971809b0..00000000 --- a/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/settings/organization/members/index.tsx +++ /dev/null @@ -1,18 +0,0 @@ -import { MembersTable } from '@/components/settings/members'; - -import { getMembers, getProjectsByOrganizationId } from '@openpanel/db'; - -interface Props { - organizationId: string; -} - -const MembersServer = async ({ organizationId }: Props) => { - const [members, projects] = await Promise.all([ - getMembers(organizationId), - getProjectsByOrganizationId(organizationId), - ]); - - return ; -}; - -export default MembersServer; diff --git a/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/settings/organization/organization/billing.tsx b/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/settings/organization/organization/billing.tsx deleted file mode 100644 index ff2ba82c..00000000 --- a/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/settings/organization/organization/billing.tsx +++ /dev/null @@ -1,302 +0,0 @@ -'use client'; - -import { Button } from '@/components/ui/button'; -import { - Dialog, - DialogClose, - DialogContent, - DialogDescription, - DialogFooter, - DialogTitle, -} from '@/components/ui/dialog'; -import { Switch } from '@/components/ui/switch'; -import { Tooltiper } from '@/components/ui/tooltip'; -import { Widget, WidgetBody, WidgetHead } from '@/components/widget'; -import { WidgetTable } from '@/components/widget-table'; -import { useAppParams } from '@/hooks/useAppParams'; -import useWS from '@/hooks/useWS'; -import { showConfirm } from '@/modals'; -import { api } from '@/trpc/client'; -import type { IServiceOrganization } from '@openpanel/db'; -import { useOpenPanel } from '@openpanel/nextjs'; -import type { IPolarPrice } from '@openpanel/payments'; -import { Loader2Icon } from 'lucide-react'; -import { useRouter } from 'next/navigation'; -import { useQueryState } from 'nuqs'; -import { useEffect, useMemo, useState } from 'react'; -import { toast } from 'sonner'; - -type Props = { - organization: IServiceOrganization; -}; - -export default function Billing({ organization }: Props) { - const router = useRouter(); - const { projectId } = useAppParams(); - const op = useOpenPanel(); - const [customerSessionToken, setCustomerSessionToken] = useQueryState( - 'customer_session_token', - ); - const productsQuery = api.subscription.products.useQuery({ - organizationId: organization.id, - }); - - useWS(`/live/organization/${organization.id}`, (event) => { - router.refresh(); - }); - - const [recurringInterval, setRecurringInterval] = useState<'year' | 'month'>( - (organization.subscriptionInterval as 'year' | 'month') || 'month', - ); - - const products = useMemo(() => { - return (productsQuery.data || []) - .filter((product) => product.recurringInterval === recurringInterval) - .filter((product) => product.prices.some((p) => p.amountType !== 'free')); - }, [productsQuery.data, recurringInterval]); - - useEffect(() => { - if (organization.subscriptionInterval) { - setRecurringInterval( - organization.subscriptionInterval as 'year' | 'month', - ); - } - }, [organization.subscriptionInterval]); - - useEffect(() => { - if (customerSessionToken) { - op.track('subscription_created'); - } - }, [customerSessionToken]); - - function renderBillingTable() { - if (productsQuery.isLoading) { - return ( -
- -
- ); - } - if (productsQuery.isError) { - return ( -
- Issues loading all tiers -
- ); - } - return ( - item.id} - columns={[ - { - name: 'Tier', - className: 'text-left', - width: 'auto', - render(item) { - return
{item.name}
; - }, - }, - { - name: 'Price', - width: 'auto', - render(item) { - const price = item.prices[0]; - if (!price) { - return null; - } - - if (price.amountType === 'free') { - return null; - // return ( - //
- //
- // Free - // - //
- //
- // ); - } - - if (price.amountType !== 'fixed') { - return null; - } - - return ( -
-
- - {new Intl.NumberFormat('en-US', { - style: 'currency', - currency: price.priceCurrency, - minimumFractionDigits: 0, - maximumFractionDigits: 1, - }).format(price.priceAmount / 100)} - {' / '} - {recurringInterval === 'year' ? 'year' : 'month'} - - -
-
- ); - }, - }, - ]} - /> - ); - } - - return ( - <> - - - Billing -
- - {recurringInterval === 'year' - ? 'Yearly (2 months free)' - : 'Monthly'} - - - setRecurringInterval(checked ? 'year' : 'month') - } - /> -
-
- -
- {renderBillingTable()} -
-

Do you need higher limits?

-

- Reach out to{' '} - - hello@openpanel.dev - {' '} - and we'll help you out. -

-
-
-
-
- { - setCustomerSessionToken(null); - if (!open) { - router.refresh(); - } - }} - > - - Subscription created - - We have registered your subscription. It'll be activated within a - couple of seconds. - - - - - - - - - - ); -} - -function CheckoutButton({ - price, - organization, - projectId, - disabled, -}: { - price: IPolarPrice; - organization: IServiceOrganization; - projectId: string; - disabled?: string | null; -}) { - const op = useOpenPanel(); - const isCurrentPrice = organization.subscriptionPriceId === price.id; - const checkout = api.subscription.checkout.useMutation({ - onSuccess(data) { - if (data?.url) { - window.location.href = data.url; - } else { - toast.success('Subscription updated', { - description: 'It might take a few seconds to update', - }); - } - }, - }); - - const isCanceled = - organization.subscriptionStatus === 'active' && - isCurrentPrice && - organization.subscriptionCanceledAt; - const isActive = - organization.subscriptionStatus === 'active' && isCurrentPrice; - - return ( - - - - ); -} diff --git a/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/settings/organization/organization/usage.tsx b/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/settings/organization/organization/usage.tsx deleted file mode 100644 index 736cb0d2..00000000 --- a/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/settings/organization/organization/usage.tsx +++ /dev/null @@ -1,288 +0,0 @@ -'use client'; - -import { - useXAxisProps, - useYAxisProps, -} from '@/components/report-chart/common/axis'; -import { Widget, WidgetBody, WidgetHead } from '@/components/widget'; -import { useNumber } from '@/hooks/useNumerFormatter'; -import { api } from '@/trpc/client'; -import { formatDate } from '@/utils/date'; -import { getChartColor } from '@/utils/theme'; -import { sum } from '@openpanel/common'; -import type { IServiceOrganization } from '@openpanel/db'; -import { Loader2Icon } from 'lucide-react'; -import { - Bar, - BarChart, - CartesianGrid, - Tooltip as RechartTooltip, - ReferenceLine, - ResponsiveContainer, - XAxis, - YAxis, -} from 'recharts'; - -type Props = { - organization: IServiceOrganization; -}; - -function Card({ title, value }: { title: string; value: string }) { - return ( -
-
{title}
-
{value}
-
- ); -} - -export default function Usage({ organization }: Props) { - const number = useNumber(); - const xAxisProps = useXAxisProps({ interval: 'day' }); - const yAxisProps = useYAxisProps({}); - const usageQuery = api.subscription.usage.useQuery({ - organizationId: organization.id, - }); - - const wrapper = (node: React.ReactNode) => ( - - - Usage - - {node} - - ); - - if (usageQuery.isLoading) { - return wrapper( -
- -
, - ); - } - if (usageQuery.isError) { - return wrapper( -
- Issues loading usage data -
, - ); - } - - const subscriptionPeriodEventsLimit = organization.hasSubscription - ? organization.subscriptionPeriodEventsLimit - : 0; - const subscriptionPeriodEventsCount = organization.hasSubscription - ? organization.subscriptionPeriodEventsCount - : 0; - - const domain = [ - 0, - Math.max( - subscriptionPeriodEventsLimit, - subscriptionPeriodEventsCount, - ...usageQuery.data.map((item) => item.count), - ), - ] as [number, number]; - - domain[1] += domain[1] * 0.05; - - return wrapper( - <> -
- {organization.hasSubscription ? ( - <> - - - - - - ) : ( - <> -
- -
-
- item.count)), - )} - /> -
- - )} -
-
- - ({ - date: new Date(item.day).getTime(), - count: item.count, - limit: subscriptionPeriodEventsLimit, - total: subscriptionPeriodEventsCount, - }))} - barSize={8} - > - - - - - - - } - cursor={{ - stroke: 'hsl(var(--def-400))', - fill: 'hsl(var(--def-200))', - }} - /> - {organization.hasSubscription && ( - <> - - 1000 - ? 'insideTop' - : 'insideBottom', - fontSize: 12, - }} - /> - - )} - - - - - - -
- , - ); -} - -function Tooltip(props: any) { - const number = useNumber(); - const payload = props.payload?.[0]?.payload; - - if (!payload) { - return null; - } - return ( -
-
- {formatDate(payload.date)} -
- {payload.limit !== 0 && ( -
-
-
-
Your tier limit
-
- {number.format(payload.limit)} -
-
-
- )} - {payload.total !== 0 && ( -
-
-
-
- Total events count -
-
- {number.format(payload.total)} -
-
-
- )} -
-
-
-
Events this day
-
- {number.format(payload.count)} -
-
-
-
- ); -} diff --git a/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/settings/organization/page.tsx b/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/settings/organization/page.tsx deleted file mode 100644 index b09e763c..00000000 --- a/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/settings/organization/page.tsx +++ /dev/null @@ -1,107 +0,0 @@ -import { FullPageEmptyState } from '@/components/full-page-empty-state'; -import { PageTabs, PageTabsLink } from '@/components/page-tabs'; -import { Padding } from '@/components/ui/padding'; -import { ShieldAlertIcon } from 'lucide-react'; -import { notFound } from 'next/navigation'; -import { parseAsStringEnum } from 'nuqs/server'; - -import { auth } from '@openpanel/auth/nextjs'; -import { db } from '@openpanel/db'; - -import InvitesServer from './invites'; -import MembersServer from './members'; -import Billing from './organization/billing'; -import { BillingFaq } from './organization/billing-faq'; -import CurrentSubscription from './organization/current-subscription'; -import Organization from './organization/organization'; -import Usage from './organization/usage'; - -interface PageProps { - params: { - organizationSlug: string; - }; - searchParams: Record; -} - -export default async function Page({ - params: { organizationSlug: organizationId }, - searchParams, -}: PageProps) { - const isBillingEnabled = process.env.NEXT_PUBLIC_SELF_HOSTED !== 'true'; - const tab = parseAsStringEnum(['org', 'billing', 'members', 'invites']) - .withDefault('org') - .parseServerSide(searchParams.tab); - const session = await auth(); - const organization = await db.organization.findUnique({ - where: { - id: organizationId, - members: { - some: { - userId: session.userId, - }, - }, - }, - include: { - members: { - select: { - role: true, - userId: true, - }, - }, - }, - }); - - if (!organization) { - return notFound(); - } - - const member = organization.members.find( - (member) => member.userId === session.userId, - ); - - const hasAccess = member?.role === 'org:admin'; - - if (!hasAccess) { - return ( - - You do not have access to this page. You need to be an admin of this - organization to access this page. - - ); - } - - return ( - - - - Organization - - {isBillingEnabled && ( - - Billing - - )} - - Members - - - Invites - - - - {tab === 'org' && } - {tab === 'billing' && isBillingEnabled && ( -
-
- - - -
- -
- )} - {tab === 'members' && } - {tab === 'invites' && } -
- ); -} diff --git a/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/settings/page.tsx b/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/settings/page.tsx deleted file mode 100644 index 22b9c8b1..00000000 --- a/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/settings/page.tsx +++ /dev/null @@ -1 +0,0 @@ -export { default } from './organization/page'; diff --git a/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/settings/profile/edit-profile.tsx b/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/settings/profile/edit-profile.tsx deleted file mode 100644 index fe828887..00000000 --- a/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/settings/profile/edit-profile.tsx +++ /dev/null @@ -1,89 +0,0 @@ -'use client'; - -import { InputWithLabel } from '@/components/forms/input-with-label'; -import { Button } from '@/components/ui/button'; -import { Widget, WidgetBody, WidgetHead } from '@/components/widget'; -import { api, handleError } from '@/trpc/client'; -import { zodResolver } from '@hookform/resolvers/zod'; -import { useRouter } from 'next/navigation'; -import { useForm } from 'react-hook-form'; -import { toast } from 'sonner'; -import { z } from 'zod'; - -import type { getUserById } from '@openpanel/db'; - -const validator = z.object({ - firstName: z.string().min(2), - lastName: z.string().min(2), - email: z.string().email(), -}); - -type IForm = z.infer; -interface EditProfileProps { - profile: Awaited>; -} -export default function EditProfile({ profile }: EditProfileProps) { - const router = useRouter(); - - const { register, handleSubmit, reset, formState } = useForm({ - resolver: zodResolver(validator), - defaultValues: { - firstName: profile.firstName ?? '', - lastName: profile.lastName ?? '', - email: profile.email ?? '', - }, - }); - - const mutation = api.user.update.useMutation({ - onSuccess(res) { - toast('Profile updated', { - description: 'Your profile has been updated.', - }); - reset({ - firstName: res.firstName ?? '', - lastName: res.lastName ?? '', - email: res.email, - }); - router.refresh(); - }, - onError: handleError, - }); - - return ( -
{ - mutation.mutate(values); - })} - > - - - Your profile - - - - - - - - -
- ); -} diff --git a/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/settings/profile/loading.tsx b/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/settings/profile/loading.tsx deleted file mode 100644 index 9cf2cf50..00000000 --- a/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/settings/profile/loading.tsx +++ /dev/null @@ -1,3 +0,0 @@ -import FullPageLoadingState from '@/components/full-page-loading-state'; - -export default FullPageLoadingState; diff --git a/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/settings/profile/logout.tsx b/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/settings/profile/logout.tsx deleted file mode 100644 index 71578074..00000000 --- a/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/settings/profile/logout.tsx +++ /dev/null @@ -1,18 +0,0 @@ -'use client'; - -import SignOutButton from '@/components/sign-out-button'; -import { Widget, WidgetBody, WidgetHead } from '@/components/widget'; - -export function Logout() { - return ( - - - Sad part - - -

Sometimes you need to go. See you next time

- -
-
- ); -} diff --git a/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/settings/profile/page.tsx b/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/settings/profile/page.tsx deleted file mode 100644 index ee5865e3..00000000 --- a/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/settings/profile/page.tsx +++ /dev/null @@ -1,17 +0,0 @@ -import { Padding } from '@/components/ui/padding'; -import { auth } from '@openpanel/auth/nextjs'; -import { getUserById } from '@openpanel/db'; - -import EditProfile from './edit-profile'; - -export default async function Page() { - const { userId } = await auth(); - const profile = await getUserById(userId!); - - return ( - -

Profile

- -
- ); -} diff --git a/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/settings/projects/loading.tsx b/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/settings/projects/loading.tsx deleted file mode 100644 index 9cf2cf50..00000000 --- a/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/settings/projects/loading.tsx +++ /dev/null @@ -1,3 +0,0 @@ -import FullPageLoadingState from '@/components/full-page-loading-state'; - -export default FullPageLoadingState; diff --git a/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/settings/projects/page.tsx b/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/settings/projects/page.tsx deleted file mode 100644 index 37699219..00000000 --- a/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/settings/projects/page.tsx +++ /dev/null @@ -1,42 +0,0 @@ -import { Padding } from '@/components/ui/padding'; - -import { - db, - getClientsByOrganizationId, - getProjectWithClients, - getProjectsByOrganizationId, -} from '@openpanel/db'; - -import { notFound } from 'next/navigation'; -import DeleteProject from './delete-project'; -import EditProjectDetails from './edit-project-details'; -import EditProjectFilters from './edit-project-filters'; -import ProjectClients from './project-clients'; - -interface PageProps { - params: { - projectId: string; - }; -} - -export default async function Page({ params: { projectId } }: PageProps) { - const project = await getProjectWithClients(projectId); - - if (!project) { - notFound(); - } - - return ( - -
-
-

{project.name}

-
- - - - -
-
- ); -} diff --git a/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/settings/projects/project-clients.tsx b/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/settings/projects/project-clients.tsx deleted file mode 100644 index e202ece9..00000000 --- a/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/settings/projects/project-clients.tsx +++ /dev/null @@ -1,45 +0,0 @@ -'use client'; - -import { ClientsTable } from '@/components/clients/table'; -import { Button } from '@/components/ui/button'; -import { Widget, WidgetBody, WidgetHead } from '@/components/widget'; -import { pushModal } from '@/modals'; -import type { - IServiceClientWithProject, - IServiceProjectWithClients, -} from '@openpanel/db'; -import { PlusIcon } from 'lucide-react'; -import { omit } from 'ramda'; - -type Props = { project: IServiceProjectWithClients }; - -export default function ProjectClients({ project }: Props) { - return ( - - - Clients - - - - ({ - ...item, - project: omit(['clients'], item), - })) as unknown as IServiceClientWithProject[], - isFetching: false, - isLoading: false, - }} - /> - - - ); -} diff --git a/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/settings/references/list-references.tsx b/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/settings/references/list-references.tsx deleted file mode 100644 index d493af55..00000000 --- a/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/settings/references/list-references.tsx +++ /dev/null @@ -1,29 +0,0 @@ -'use client'; - -import { DataTable } from '@/components/data-table'; -import { columns } from '@/components/references/table'; -import { Button } from '@/components/ui/button'; -import { Padding } from '@/components/ui/padding'; -import { pushModal } from '@/modals'; -import { PlusIcon } from 'lucide-react'; - -import type { IServiceReference } from '@openpanel/db'; - -interface ListProjectsProps { - data: IServiceReference[]; -} - -export default function ListReferences({ data }: ListProjectsProps) { - return ( - -
-

References

- -
- -
- ); -} diff --git a/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/settings/references/loading.tsx b/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/settings/references/loading.tsx deleted file mode 100644 index 9cf2cf50..00000000 --- a/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/settings/references/loading.tsx +++ /dev/null @@ -1,3 +0,0 @@ -import FullPageLoadingState from '@/components/full-page-loading-state'; - -export default FullPageLoadingState; diff --git a/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/settings/references/page.tsx b/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/settings/references/page.tsx deleted file mode 100644 index e32df231..00000000 --- a/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/settings/references/page.tsx +++ /dev/null @@ -1,25 +0,0 @@ -import { getReferences } from '@openpanel/db'; - -import ListReferences from './list-references'; - -interface PageProps { - params: { - projectId: string; - }; -} - -export default async function Page({ params: { projectId } }: PageProps) { - const references = await getReferences({ - where: { - projectId, - }, - take: 50, - skip: 0, - }); - - return ( - <> - - - ); -} diff --git a/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/side-effects-free-plan.tsx b/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/side-effects-free-plan.tsx deleted file mode 100644 index 133cdfe3..00000000 --- a/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/side-effects-free-plan.tsx +++ /dev/null @@ -1,61 +0,0 @@ -'use client'; - -import { differenceInHours } from 'date-fns'; -import { useEffect, useState } from 'react'; - -import { ProjectLink } from '@/components/links'; -import { Dialog, DialogContent } from '@/components/ui/dialog'; -import { ModalHeader } from '@/modals/Modal/Container'; -import type { IServiceOrganization } from '@openpanel/db'; -import { useOpenPanel } from '@openpanel/nextjs'; -import { FREE_PRODUCT_IDS } from '@openpanel/payments'; -import Billing from './settings/organization/organization/billing'; - -interface SideEffectsProps { - organization: IServiceOrganization; -} - -export default function SideEffectsFreePlan({ - organization, -}: SideEffectsProps) { - const op = useOpenPanel(); - const willEndInHours = organization.subscriptionEndsAt - ? differenceInHours(organization.subscriptionEndsAt, new Date()) - : null; - const [isFreePlan, setIsFreePlan] = useState( - !!organization.subscriptionProductId && - FREE_PRODUCT_IDS.includes(organization.subscriptionProductId), - ); - - useEffect(() => { - if (isFreePlan) { - op.track('free_plan_removed'); - } - }, []); - - return ( - - - setIsFreePlan(false)} - title={'Free plan has been removed'} - text={ - <> - Please upgrade your plan to continue using OpenPanel. Select a - tier which is appropriate for your needs or{' '} - - manage billing - - - } - /> -
- -
-
-
- ); -} diff --git a/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/side-effects-timezone.tsx b/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/side-effects-timezone.tsx deleted file mode 100644 index b5b46ebf..00000000 --- a/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/side-effects-timezone.tsx +++ /dev/null @@ -1,91 +0,0 @@ -'use client'; - -import { useState } from 'react'; - -import { Button } from '@/components/ui/button'; -import { Combobox } from '@/components/ui/combobox'; -import { Dialog, DialogContent, DialogFooter } from '@/components/ui/dialog'; -import { ModalHeader } from '@/modals/Modal/Container'; -import { api, handleError } from '@/trpc/client'; -import { TIMEZONES } from '@openpanel/common'; -import type { IServiceOrganization } from '@openpanel/db'; -import { toast } from 'sonner'; - -interface SideEffectsProps { - organization: IServiceOrganization; -} - -export default function SideEffectsTimezone({ - organization, -}: SideEffectsProps) { - const [isMissingTimezone, setIsMissingTimezone] = useState( - !organization.timezone, - ); - const defaultTimezone = Intl.DateTimeFormat().resolvedOptions().timeZone; - const [timezone, setTimezone] = useState( - TIMEZONES.includes(defaultTimezone) ? defaultTimezone : '', - ); - - const mutation = api.organization.update.useMutation({ - onSuccess(res) { - toast('Timezone updated', { - description: 'Your timezone has been updated.', - }); - window.location.reload(); - }, - onError: handleError, - }); - - return ( - - { - e.preventDefault(); - }} - onInteractOutside={(e) => { - e.preventDefault(); - }} - > - - We have introduced new features that requires your timezone. - Please select the timezone you want to use for your organization. - - } - /> - ({ - value: item, - label: item, - }))} - value={timezone} - onChange={setTimezone} - placeholder="Select a timezone" - searchable - size="lg" - className="w-full px-4" - /> - - - - - - ); -} diff --git a/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/side-effects-trial.tsx b/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/side-effects-trial.tsx deleted file mode 100644 index e557ae2a..00000000 --- a/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/side-effects-trial.tsx +++ /dev/null @@ -1,67 +0,0 @@ -'use client'; - -import { differenceInHours } from 'date-fns'; -import { useEffect, useState } from 'react'; - -import { ProjectLink } from '@/components/links'; -import { Dialog, DialogContent } from '@/components/ui/dialog'; -import { ModalHeader } from '@/modals/Modal/Container'; -import type { IServiceOrganization } from '@openpanel/db'; -import { useOpenPanel } from '@openpanel/nextjs'; -import Billing from './settings/organization/organization/billing'; - -interface SideEffectsProps { - organization: IServiceOrganization; -} - -export default function SideEffectsTrial({ organization }: SideEffectsProps) { - const op = useOpenPanel(); - const willEndInHours = organization.subscriptionEndsAt - ? differenceInHours(organization.subscriptionEndsAt, new Date()) - : null; - - const [isTrialDialogOpen, setIsTrialDialogOpen] = useState( - willEndInHours !== null && - organization.subscriptionStatus === 'trialing' && - organization.subscriptionEndsAt !== null && - willEndInHours <= 48, - ); - - useEffect(() => { - if (isTrialDialogOpen) { - op.track('trial_expires_soon'); - } - }, [isTrialDialogOpen]); - - return ( - <> - - - setIsTrialDialogOpen(false)} - title={ - willEndInHours !== null && willEndInHours > 0 - ? `Your trial is ending in ${willEndInHours} hours` - : 'Your trial has ended' - } - text={ - <> - Please upgrade your plan to continue using OpenPanel. Select a - tier which is appropriate for your needs or{' '} - - manage billing - - - } - /> -
- -
-
-
- - ); -} diff --git a/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/side-effects.tsx b/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/side-effects.tsx deleted file mode 100644 index 07267c0d..00000000 --- a/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/side-effects.tsx +++ /dev/null @@ -1,41 +0,0 @@ -'use client'; - -import { differenceInHours } from 'date-fns'; -import { useEffect, useState } from 'react'; - -import { ProjectLink } from '@/components/links'; -import { Combobox } from '@/components/ui/combobox'; -import { Dialog, DialogContent } from '@/components/ui/dialog'; -import { ModalHeader } from '@/modals/Modal/Container'; -import type { IServiceOrganization } from '@openpanel/db'; -import { useOpenPanel } from '@openpanel/nextjs'; -import { FREE_PRODUCT_IDS } from '@openpanel/payments'; -import Billing from './settings/organization/organization/billing'; -import SideEffectsFreePlan from './side-effects-free-plan'; -import SideEffectsTimezone from './side-effects-timezone'; -import SideEffectsTrial from './side-effects-trial'; - -interface SideEffectsProps { - organization: IServiceOrganization; -} - -export default function SideEffects({ organization }: SideEffectsProps) { - const [mounted, setMounted] = useState(false); - - useEffect(() => { - setMounted(true); - }, []); - - // Avoids hydration errors - if (!mounted) { - return null; - } - - return ( - <> - - - - - ); -} diff --git a/apps/dashboard/src/app/(app)/[organizationSlug]/page.tsx b/apps/dashboard/src/app/(app)/[organizationSlug]/page.tsx deleted file mode 100644 index 39946c91..00000000 --- a/apps/dashboard/src/app/(app)/[organizationSlug]/page.tsx +++ /dev/null @@ -1,65 +0,0 @@ -import { FullPageEmptyState } from '@/components/full-page-empty-state'; -import FullWidthNavbar from '@/components/full-width-navbar'; -import ProjectCard from '@/components/projects/project-card'; -import { redirect } from 'next/navigation'; - -import SettingsToggle from '@/components/settings-toggle'; -import { auth } from '@openpanel/auth/nextjs'; -import { getOrganizations, getProjects } from '@openpanel/db'; -import LayoutProjectSelector from './[projectId]/layout-project-selector'; - -interface PageProps { - params: { - organizationSlug: string; - }; -} - -export default async function Page({ - params: { organizationSlug: organizationId }, -}: PageProps) { - const { userId } = await auth(); - const [organizations, projects] = await Promise.all([ - getOrganizations(userId), - getProjects({ organizationId, userId }), - ]); - - const organization = organizations.find((org) => org.id === organizationId); - - if (!organization) { - return ( - - The organization you were looking for could not be found. - - ); - } - - if (projects.length === 0) { - return redirect('/onboarding/project'); - } - - if (projects.length === 1 && projects[0]) { - return redirect(`/${organizationId}/${projects[0].id}`); - } - - return ( -
- -
- - -
-
-
-
- {projects.map((item) => ( - - ))} -
-
-
- ); -} diff --git a/apps/dashboard/src/app/(app)/page.tsx b/apps/dashboard/src/app/(app)/page.tsx deleted file mode 100644 index d3228445..00000000 --- a/apps/dashboard/src/app/(app)/page.tsx +++ /dev/null @@ -1,15 +0,0 @@ -import { redirect } from 'next/navigation'; - -import { auth } from '@openpanel/auth/nextjs'; -import { getOrganizations } from '@openpanel/db'; - -export default async function Page() { - const { userId } = await auth(); - const organizations = await getOrganizations(userId); - - if (organizations.length > 0) { - return redirect(`/${organizations[0]?.id}`); - } - - return redirect('/onboarding/project'); -} diff --git a/apps/dashboard/src/app/(auth)/layout.tsx b/apps/dashboard/src/app/(auth)/layout.tsx deleted file mode 100644 index c1c2128a..00000000 --- a/apps/dashboard/src/app/(auth)/layout.tsx +++ /dev/null @@ -1,22 +0,0 @@ -import LiveEventsServer from './live-events'; - -type Props = { - children: React.ReactNode; -}; - -const Page = ({ children }: Props) => { - return ( - <> -
-
-
- -
-
{children}
-
-
- - ); -}; - -export default Page; diff --git a/apps/dashboard/src/app/(auth)/live-events/index.tsx b/apps/dashboard/src/app/(auth)/live-events/index.tsx deleted file mode 100644 index e056be64..00000000 --- a/apps/dashboard/src/app/(auth)/live-events/index.tsx +++ /dev/null @@ -1,7 +0,0 @@ -import LiveEvents from './live-events'; - -const LiveEventsServer = () => { - return ; -}; - -export default LiveEventsServer; diff --git a/apps/dashboard/src/app/(auth)/login/page.tsx b/apps/dashboard/src/app/(auth)/login/page.tsx deleted file mode 100644 index 9cbc906e..00000000 --- a/apps/dashboard/src/app/(auth)/login/page.tsx +++ /dev/null @@ -1,64 +0,0 @@ -import { Or } from '@/components/auth/or'; -import { SignInEmailForm } from '@/components/auth/sign-in-email-form'; -import { SignInGithub } from '@/components/auth/sign-in-github'; -import { SignInGoogle } from '@/components/auth/sign-in-google'; -import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert'; -import { LinkButton } from '@/components/ui/button'; -import { auth } from '@openpanel/auth/nextjs'; -import { AlertCircle } from 'lucide-react'; -import { redirect } from 'next/navigation'; - -export default async function Page({ - searchParams, -}: { - searchParams: { error?: string; correlationId?: string }; -}) { - const session = await auth(); - const error = searchParams.error; - const correlationId = searchParams.correlationId; - - if (session.userId) { - return redirect('/'); - } - - return ( -
-
- {error && ( - - - Error - -

{error}

- {correlationId && ( - <> -

Correlation ID: {correlationId}

-

- Contact us if you have any issues.{' '} - - hello[at]openpanel.dev - -

- - )} -
-
- )} -
- - -
- -
- -
- - No account? Sign up today - -
-
- ); -} diff --git a/apps/dashboard/src/app/(auth)/reset-password/page.tsx b/apps/dashboard/src/app/(auth)/reset-password/page.tsx deleted file mode 100644 index 84f47245..00000000 --- a/apps/dashboard/src/app/(auth)/reset-password/page.tsx +++ /dev/null @@ -1,17 +0,0 @@ -import { ResetPasswordForm } from '@/components/auth/reset-password-form'; -import { auth } from '@openpanel/auth/nextjs'; -import { redirect } from 'next/navigation'; - -export default async function Page() { - const session = await auth(); - - if (session.userId) { - return redirect('/'); - } - - return ( -
- -
- ); -} diff --git a/apps/dashboard/src/app/(onboarding)/layout.tsx b/apps/dashboard/src/app/(onboarding)/layout.tsx deleted file mode 100644 index 348ce998..00000000 --- a/apps/dashboard/src/app/(onboarding)/layout.tsx +++ /dev/null @@ -1,42 +0,0 @@ -import FullWidthNavbar from '@/components/full-width-navbar'; - -import SkipOnboarding from './skip-onboarding'; -import Steps from './steps'; - -type Props = { - children: React.ReactNode; -}; - -const Page = ({ children }: Props) => { - return ( - <> -
-
-
-
-
- - - -
-
-
-
-
- Welcome to Openpanel -
-
- Get started -
-
- -
-
{children}
-
-
-
- - ); -}; - -export default Page; diff --git a/apps/dashboard/src/app/(onboarding)/onboarding-layout.tsx b/apps/dashboard/src/app/(onboarding)/onboarding-layout.tsx deleted file mode 100644 index 7d328940..00000000 --- a/apps/dashboard/src/app/(onboarding)/onboarding-layout.tsx +++ /dev/null @@ -1,42 +0,0 @@ -import { cn } from '@/utils/cn'; - -type Props = { - children: React.ReactNode; - className?: string; - title: string; - description?: React.ReactNode; -}; - -export const OnboardingDescription = ({ - children, - className, -}: Pick) => ( -
- {children} -
-); - -const OnboardingLayout = ({ - title, - description, - children, - className, -}: Props) => { - return ( -
-
-

{title}

- {description} -
- - {children} -
- ); -}; - -export default OnboardingLayout; diff --git a/apps/dashboard/src/app/(onboarding)/onboarding/[projectId]/connect/onboarding-connect.tsx b/apps/dashboard/src/app/(onboarding)/onboarding/[projectId]/connect/onboarding-connect.tsx deleted file mode 100644 index a1e8cf99..00000000 --- a/apps/dashboard/src/app/(onboarding)/onboarding/[projectId]/connect/onboarding-connect.tsx +++ /dev/null @@ -1,70 +0,0 @@ -'use client'; - -import { ButtonContainer } from '@/components/button-container'; -import CopyInput from '@/components/forms/copy-input'; -import { LinkButton } from '@/components/ui/button'; -import { useClientSecret } from '@/hooks/useClientSecret'; -import { LockIcon } from 'lucide-react'; - -import type { IServiceProjectWithClients } from '@openpanel/db'; - -import OnboardingLayout, { - OnboardingDescription, -} from '../../../onboarding-layout'; -import ConnectApp from './connect-app'; -import ConnectBackend from './connect-backend'; -import ConnectWeb from './connect-web'; - -type Props = { - project: IServiceProjectWithClients; -}; - -const Connect = ({ project }: Props) => { - const client = project.clients[0]; - const [secret] = useClientSecret(); - - if (!client) { - return
Hmm, something fishy is going on. Please reload the page.
; - } - - return ( - - Let's connect your data sources to OpenPanel - - } - > -
-
- - Credentials -
- - -
- {project.types.map((type) => { - const Component = { - website: ConnectWeb, - app: ConnectApp, - backend: ConnectBackend, - }[type]; - - return ; - })} - -
- - Next - - - - ); -}; - -export default Connect; diff --git a/apps/dashboard/src/app/(onboarding)/onboarding/[projectId]/connect/page.tsx b/apps/dashboard/src/app/(onboarding)/onboarding/[projectId]/connect/page.tsx deleted file mode 100644 index 9569c2fb..00000000 --- a/apps/dashboard/src/app/(onboarding)/onboarding/[projectId]/connect/page.tsx +++ /dev/null @@ -1,28 +0,0 @@ -import { getOrganizations, getProjectWithClients } from '@openpanel/db'; - -import { auth } from '@openpanel/auth/nextjs'; -import OnboardingConnect from './onboarding-connect'; - -type Props = { - params: { - projectId: string; - }; -}; - -const Connect = async ({ params: { projectId } }: Props) => { - const { userId } = await auth(); - const orgs = await getOrganizations(userId); - const organizationId = orgs[0]?.id; - if (!organizationId) { - throw new Error('No organization found'); - } - const project = await getProjectWithClients(projectId); - - if (!project) { - return
Hmm, something fishy is going on. Please reload the page.
; - } - - return ; -}; - -export default Connect; diff --git a/apps/dashboard/src/app/(onboarding)/onboarding/[projectId]/verify/onboarding-verify.tsx b/apps/dashboard/src/app/(onboarding)/onboarding/[projectId]/verify/onboarding-verify.tsx deleted file mode 100644 index 8264cc9a..00000000 --- a/apps/dashboard/src/app/(onboarding)/onboarding/[projectId]/verify/onboarding-verify.tsx +++ /dev/null @@ -1,155 +0,0 @@ -'use client'; - -import { ButtonContainer } from '@/components/button-container'; -import { LinkButton } from '@/components/ui/button'; -import { cn } from '@/utils/cn'; -import Link from 'next/link'; -import { useEffect, useState } from 'react'; - -import type { - IServiceClient, - IServiceEvent, - IServiceProjectWithClients, -} from '@openpanel/db'; - -import Syntax from '@/components/syntax'; -import { - Accordion, - AccordionContent, - AccordionItem, - AccordionTrigger, -} from '@/components/ui/accordion'; -import { useClientSecret } from '@/hooks/useClientSecret'; -import { clipboard } from '@/utils/clipboard'; -import { local } from 'd3'; -import OnboardingLayout, { - OnboardingDescription, -} from '../../../onboarding-layout'; -import VerifyListener from './onboarding-verify-listener'; - -type Props = { - project: IServiceProjectWithClients; - events: IServiceEvent[]; -}; - -const Verify = ({ project, events }: Props) => { - const [verified, setVerified] = useState(events.length > 0); - const client = project.clients[0]; - - if (!client) { - return
Hmm, something fishy is going on. Please reload the page.
; - } - - return ( - - Deploy your changes, as soon as you see events here, you're all - set! - - } - > - - - - - - - Back - - -
- {!verified && ( - - Skip for now - - )} - - - Your dashboard - -
-
-
- ); -}; - -export default Verify; - -function CurlPreview({ project }: { project: IServiceProjectWithClients }) { - const [secret] = useClientSecret(); - const client = project.clients[0]; - if (!client) { - return null; - } - - const payload: Record = { - type: 'track', - payload: { - name: 'screen_view', - properties: { - __title: `Testing OpenPanel - ${project.name}`, - __path: `${project.domain}`, - __referrer: `${process.env.NEXT_PUBLIC_DASHBOARD_URL}`, - }, - }, - }; - - if (project.types.includes('app')) { - payload.payload.properties.__path = '/'; - delete payload.payload.properties.__referrer; - } - - if (project.types.includes('backend')) { - payload.payload.name = 'test_event'; - payload.payload.properties = {}; - } - - const code = `curl -X POST ${process.env.NEXT_PUBLIC_API_URL}/track \\ --H "Content-Type: application/json" \\ --H "openpanel-client-id: ${client.id}" \\ --H "openpanel-client-secret: ${secret}" \\ --H "User-Agent: ${typeof window !== 'undefined' ? window.navigator.userAgent : ''}" \\ --d '${JSON.stringify(payload)}'`; - - return ( -
- - - { - clipboard(code, null); - }} - > - Try out the curl command - - - - - - -
- ); -} diff --git a/apps/dashboard/src/app/(onboarding)/onboarding/[projectId]/verify/page.tsx b/apps/dashboard/src/app/(onboarding)/onboarding/[projectId]/verify/page.tsx deleted file mode 100644 index 1d553e7f..00000000 --- a/apps/dashboard/src/app/(onboarding)/onboarding/[projectId]/verify/page.tsx +++ /dev/null @@ -1,41 +0,0 @@ -import { cookies } from 'next/headers'; -import { escape } from 'sqlstring'; - -import { - TABLE_NAMES, - getEvents, - getOrganizations, - getProjectWithClients, -} from '@openpanel/db'; - -import { auth } from '@openpanel/auth/nextjs'; -import OnboardingVerify from './onboarding-verify'; - -type Props = { - params: { - projectId: string; - }; -}; - -const Verify = async ({ params: { projectId } }: Props) => { - const { userId } = await auth(); - const orgs = await getOrganizations(userId); - const organizationId = orgs[0]?.id; - if (!organizationId) { - throw new Error('No organization found'); - } - const [project, events] = await Promise.all([ - await getProjectWithClients(projectId), - getEvents( - `SELECT * FROM ${TABLE_NAMES.events} WHERE project_id = ${escape(projectId)} ORDER BY created_at DESC LIMIT 100`, - ), - ]); - - if (!project) { - return
Hmm, something fishy is going on. Please reload the page.
; - } - - return ; -}; - -export default Verify; diff --git a/apps/dashboard/src/app/(onboarding)/onboarding/page.tsx b/apps/dashboard/src/app/(onboarding)/onboarding/page.tsx deleted file mode 100644 index b4d55130..00000000 --- a/apps/dashboard/src/app/(onboarding)/onboarding/page.tsx +++ /dev/null @@ -1,78 +0,0 @@ -import { Or } from '@/components/auth/or'; -import { SignInGithub } from '@/components/auth/sign-in-github'; -import { SignInGoogle } from '@/components/auth/sign-in-google'; -import { SignUpEmailForm } from '@/components/auth/sign-up-email-form'; -import { auth } from '@openpanel/auth/nextjs'; -import { getInviteById } from '@openpanel/db'; -import Link from 'next/link'; -import { redirect } from 'next/navigation'; -import OnboardingLayout, { OnboardingDescription } from '../onboarding-layout'; - -const Page = async ({ - searchParams, -}: { searchParams: { inviteId: string } }) => { - const session = await auth(); - const inviteId = await searchParams.inviteId; - const invite = inviteId ? await getInviteById(inviteId) : null; - const hasInviteExpired = invite?.expiresAt && invite.expiresAt < new Date(); - if (session.userId) { - return redirect('/'); - } - - return ( -
- - Lets start with creating you account. By creating an account you - accept the{' '} - - Terms of Service - {' '} - and{' '} - - Privacy Policy - - . - - } - > - {invite && !hasInviteExpired && ( -
-

- Invitation to {invite.organization.name} -

-

- After you have created your account, you will be added to the - organization. -

-
- )} - {invite && hasInviteExpired && ( -
-

- Invitation to {invite.organization.name} has expired -

-

- The invitation has expired. Please contact the organization owner - to get a new invitation. -

-
- )} -
- - -
- -
-

Sign up with email

- -
-
-
- ); -}; - -export default Page; diff --git a/apps/dashboard/src/app/(onboarding)/onboarding/project/page.tsx b/apps/dashboard/src/app/(onboarding)/onboarding/project/page.tsx deleted file mode 100644 index d25c25db..00000000 --- a/apps/dashboard/src/app/(onboarding)/onboarding/project/page.tsx +++ /dev/null @@ -1,11 +0,0 @@ -import { auth } from '@openpanel/auth/nextjs'; -import { getOrganizations } from '@openpanel/db'; -import { OnboardingCreateProject } from './onboarding-create-project'; - -const Page = async () => { - const { userId } = await auth(); - const organizations = await getOrganizations(userId); - return ; -}; - -export default Page; diff --git a/apps/dashboard/src/app/(public)/share/overview/[id]/page.tsx b/apps/dashboard/src/app/(public)/share/overview/[id]/page.tsx deleted file mode 100644 index 70eff2ee..00000000 --- a/apps/dashboard/src/app/(public)/share/overview/[id]/page.tsx +++ /dev/null @@ -1,87 +0,0 @@ -import { StickyBelowHeader } from '@/app/(app)/[organizationSlug]/[projectId]/layout-sticky-below-header'; -import { OverviewFiltersButtons } from '@/components/overview/filters/overview-filters-buttons'; -import ServerLiveCounter from '@/components/overview/live-counter'; -import OverviewMetrics from '@/components/overview/overview-metrics'; -import OverviewTopDevices from '@/components/overview/overview-top-devices'; -import OverviewTopEvents from '@/components/overview/overview-top-events'; -import OverviewTopGeo from '@/components/overview/overview-top-geo'; -import OverviewTopPages from '@/components/overview/overview-top-pages'; -import OverviewTopSources from '@/components/overview/overview-top-sources'; -import { notFound } from 'next/navigation'; - -import { ShareEnterPassword } from '@/components/auth/share-enter-password'; -import { OverviewRange } from '@/components/overview/overview-range'; -import { getOrganizationById, getShareOverviewById } from '@openpanel/db'; -import { cookies } from 'next/headers'; - -interface PageProps { - params: { - id: string; - }; - searchParams: { - header: string; - }; -} - -export default async function Page({ - params: { id }, - searchParams, -}: PageProps) { - const share = await getShareOverviewById(id); - if (!share) { - return notFound(); - } - if (!share.public) { - return notFound(); - } - const projectId = share.projectId; - const organization = await getOrganizationById(share.organizationId); - - if (share.password) { - const cookie = cookies().get(`shared-overview-${share.id}`)?.value; - if (!cookie) { - return ; - } - } - - return ( -
- {searchParams.header !== '0' && ( -
-
- {organization?.name} -

{share.project?.name}

-
- - POWERED BY - openpanel.dev - -
- )} -
- -
-
- -
-
- -
-
- -
-
- - - - - - -
-
-
- ); -} diff --git a/apps/dashboard/src/app/api/healthcheck/route.tsx b/apps/dashboard/src/app/api/healthcheck/route.tsx deleted file mode 100644 index f8da9360..00000000 --- a/apps/dashboard/src/app/api/healthcheck/route.tsx +++ /dev/null @@ -1,5 +0,0 @@ -export const dynamic = 'force-dynamic'; // no caching - -export async function GET(request: Request) { - return Response.json({ status: 'ok' }); -} diff --git a/apps/dashboard/src/app/favicon.ico b/apps/dashboard/src/app/favicon.ico deleted file mode 100644 index 403a07ef9b3f1dd8a2031c3d51aaa4e55c77cfc0..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 15406 zcmeHNX>b%(5FU^J>JR^?EEA(Bf(oUgLV2L2Qsq%uVs1Yy_Bf>4Y!$buZL<(OF}N6D?z z93_Pj{6U64avz98?*`Y5=U9<^_Hasm<^IsG|?+`;jCswzNLqWX9F!buQE-UrU<16Y}S`XKUj z167WoofrqbKk z+D2<#_>X!JN8Fo8lY#!E_xgf-=CHoz86RTq7I4pbjjP}D>2MQAFR_oqe{Rt}VXlHS?qP-@Tk=K2K8OSbw|%L%k=knRXSN^3@~p`p*~`#Al)q&a zFMsh6?z?A6d0OpvPI?@q36FAhX?{?^!j+zUX==hW%trlUxIyN-eE!I+#=`lpsZ_CX^# zdv@U2gO|y#%c1S7IiBd>d%(Y@5<1Fo z0j|myK^%R5`gaF-$3u5hFAA zs0AnbkW*s8`>ycbdQkmtP$MCB4-?o}1!DP?Z3Fp~0~?kwnfir|#RK{B$(Kw%e5-c& zYG);Ik^h_e(271AWn?R1^vS222u}d-Ma{l;!V)>W!?GH>U&;nna~&ea7zBS<2B656 za+4L(J)HmR*$J4U$M;yWP|So`b~fHA4uNbuA;lE5`(_ps5%W0k9#}~vab5)H`%iPY za*aO_kICS`Ay&HkcU8Us(%3#2tMM{~ey_tAi?%onqGx@8z8?pUdrg3B3;l+*lbPV(6!Aidj1r_U|^zGgV=Jt_Y6$?!kaA^f!FP+XK}L5a0r zIo_v69U7md@%h6~egy?%v9v^SZ05Plh}G&jZ}LJ~wD8B{2=z(~59If17q?+tP%NW4sUaWR`)aWogfFmeK8r)- zXi;B*4ON_vHP`rQe^2q9=0tlmx#2k{&Am23RuGY>azM0;%W zb0+|`DnMUy=(n{A@tV=&yRE3fxPOyU+D|V|A4c6Id!|@iT8Gk3h&{4@8_@VS*BLpm z&9WL_%@m!M_Ec8-Qll6j(2=tmj$?|kpuIr532mOnj5Mxo%`&#ZyL?PS6|{fl=62lg zMUL)FpbPKbq_@#nXS&aHrt*LL30qev27u0gw8vBtC7_k^N0{Rv|W$qNf&Q2I_h7F3GmfS20AzSKA*_W@ibPF z@o?oBH$c96+J{i=vT=WHq@%ggJ?C|B&wiDggN-!yvwe4K+kkBYwhdhMHqe!Qkc6FL z9@1MsI~!d*7NRT14^Rw*17jh&QXAq}S^S0;gJOt<=*;7R*}E}*hVa|T)<8!!@E@4* B`s)Ay diff --git a/apps/dashboard/src/app/global-error.tsx b/apps/dashboard/src/app/global-error.tsx deleted file mode 100644 index 2b7fa1b3..00000000 --- a/apps/dashboard/src/app/global-error.tsx +++ /dev/null @@ -1,20 +0,0 @@ -'use client'; - -import { useEffect } from 'react'; - -export default function GlobalError({ - error, -}: { - error: Error & { digest?: string }; - reset: () => void; -}) { - useEffect(() => {}, [error]); - - return ( - - -

Something went wrong

- - - ); -} diff --git a/apps/dashboard/src/app/layout.tsx b/apps/dashboard/src/app/layout.tsx deleted file mode 100644 index a4f84b64..00000000 --- a/apps/dashboard/src/app/layout.tsx +++ /dev/null @@ -1,48 +0,0 @@ -import { cn } from '@/utils/cn'; -import NextTopLoader from 'nextjs-toploader'; - -import Providers from './providers'; - -import '@/styles/globals.css'; -import 'flag-icons/css/flag-icons.min.css'; -import 'katex/dist/katex.min.css'; - -import { GeistMono } from 'geist/font/mono'; -import { GeistSans } from 'geist/font/sans'; - -export const metadata = { - title: 'Overview - Openpanel.dev', -}; - -export const viewport = { - width: 'device-width', - initialScale: 1, - maximumScale: 1, - userScalable: 1, -}; - -export default function RootLayout({ - children, -}: { - children: React.ReactNode; -}) { - return ( - - - - {children} - - - ); -} diff --git a/apps/dashboard/src/app/maintenance/page.tsx b/apps/dashboard/src/app/maintenance/page.tsx deleted file mode 100644 index a0529fad..00000000 --- a/apps/dashboard/src/app/maintenance/page.tsx +++ /dev/null @@ -1,18 +0,0 @@ -import { CalendarCogIcon } from 'lucide-react'; - -export default function Maintenance() { - return ( -
-
- -
- Oh no! -
-

Maintenance

-

- We're doing a planned maintenance. Please check back later. -

-
-
- ); -} diff --git a/apps/dashboard/src/app/manifest.ts b/apps/dashboard/src/app/manifest.ts deleted file mode 100644 index 4fd3b21a..00000000 --- a/apps/dashboard/src/app/manifest.ts +++ /dev/null @@ -1,16 +0,0 @@ -import type { MetadataRoute } from 'next'; - -export const dynamic = 'static'; - -export default function manifest(): MetadataRoute.Manifest { - return { - id: process.env.NEXT_PUBLIC_DASHBOARD_URL, - name: 'Openpanel.dev', - short_name: 'Openpanel.dev', - description: '', - start_url: '/', - display: 'standalone', - background_color: '#fff', - theme_color: '#fff', - }; -} diff --git a/apps/dashboard/src/app/providers.tsx b/apps/dashboard/src/app/providers.tsx deleted file mode 100644 index 1ea1cbec..00000000 --- a/apps/dashboard/src/app/providers.tsx +++ /dev/null @@ -1,90 +0,0 @@ -'use client'; - -import { NotificationProvider } from '@/components/notifications/notification-provider'; -import { TooltipProvider } from '@/components/ui/tooltip'; -import { ModalProvider } from '@/modals'; -import type { AppStore } from '@/redux'; -import makeStore from '@/redux'; -import { api } from '@/trpc/client'; -import { OpenPanelComponent } from '@openpanel/nextjs'; -import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; -import { httpLink } from '@trpc/client'; -import { ThemeProvider } from 'next-themes'; -import { NuqsAdapter } from 'nuqs/adapters/next/app'; -import { useRef, useState } from 'react'; -import { Provider as ReduxProvider } from 'react-redux'; -import { Toaster } from 'sonner'; -import superjson from 'superjson'; - -function AllProviders({ children }: { children: React.ReactNode }) { - const [queryClient] = useState( - () => - new QueryClient({ - defaultOptions: { - queries: { - networkMode: 'always', - refetchOnMount: true, - refetchOnWindowFocus: false, - }, - }, - }), - ); - const [trpcClient] = useState(() => - api.createClient({ - transformer: superjson, - links: [ - httpLink({ - url: `${process.env.NEXT_PUBLIC_API_URL}/trpc`, - fetch(url, options) { - return fetch(url, { - ...options, - credentials: 'include', - mode: 'cors', - }); - }, - }), - ], - }), - ); - - const storeRef = useRef(); - if (!storeRef.current) { - // Create the store instance the first time this renders - storeRef.current = makeStore(); - } - - return ( - - - {process.env.NEXT_PUBLIC_OP_CLIENT_ID && ( - - )} - - - - - {children} - - - - - - - - - - ); -} - -export default function Providers({ children }: { children: React.ReactNode }) { - return {children}; -} diff --git a/apps/dashboard/src/app/robots.txt b/apps/dashboard/src/app/robots.txt deleted file mode 100644 index 77470cb3..00000000 --- a/apps/dashboard/src/app/robots.txt +++ /dev/null @@ -1,2 +0,0 @@ -User-agent: * -Disallow: / \ No newline at end of file diff --git a/apps/dashboard/src/components/auth/or.tsx b/apps/dashboard/src/components/auth/or.tsx deleted file mode 100644 index c6bd2450..00000000 --- a/apps/dashboard/src/components/auth/or.tsx +++ /dev/null @@ -1,11 +0,0 @@ -import { cn } from '@/utils/cn'; - -export function Or({ className }: { className?: string }) { - return ( -
-
- OR -
-
- ); -} diff --git a/apps/dashboard/src/components/auth/reset-password-form.tsx b/apps/dashboard/src/components/auth/reset-password-form.tsx deleted file mode 100644 index d696ef3b..00000000 --- a/apps/dashboard/src/components/auth/reset-password-form.tsx +++ /dev/null @@ -1,57 +0,0 @@ -'use client'; - -import { InputWithLabel } from '@/components/forms/input-with-label'; -import { Button } from '@/components/ui/button'; -import { api } from '@/trpc/client'; -import { zodResolver } from '@hookform/resolvers/zod'; -import { zResetPassword } from '@openpanel/validation'; -import { useRouter, useSearchParams } from 'next/navigation'; -import { useForm } from 'react-hook-form'; -import { toast } from 'sonner'; -import type { z } from 'zod'; - -const validator = zResetPassword; -type IForm = z.infer; - -export function ResetPasswordForm() { - const searchParams = useSearchParams(); - const token = searchParams.get('token') ?? null; - const router = useRouter(); - const mutation = api.auth.resetPassword.useMutation({ - onSuccess() { - toast.success('Password reset successfully', { - description: 'You can now login with your new password', - }); - router.push('/login'); - }, - onError(error) { - toast.error(error.message); - }, - }); - - const form = useForm({ - resolver: zodResolver(validator), - defaultValues: { - token: token ?? '', - password: '', - }, - }); - - const onSubmit = form.handleSubmit(async (data) => { - mutation.mutate(data); - }); - - return ( -
-
- - - -
- ); -} diff --git a/apps/dashboard/src/components/clients/client-actions.tsx b/apps/dashboard/src/components/clients/client-actions.tsx deleted file mode 100644 index f4bdc151..00000000 --- a/apps/dashboard/src/components/clients/client-actions.tsx +++ /dev/null @@ -1,73 +0,0 @@ -'use client'; - -import { pushModal, showConfirm } from '@/modals'; -import { api } from '@/trpc/client'; -import { clipboard } from '@/utils/clipboard'; -import { MoreHorizontal } from 'lucide-react'; -import { useRouter } from 'next/navigation'; -import { toast } from 'sonner'; - -import type { IServiceClientWithProject } from '@openpanel/db'; - -import { Button } from '../ui/button'; -import { - DropdownMenu, - DropdownMenuContent, - DropdownMenuItem, - DropdownMenuLabel, - DropdownMenuSeparator, - DropdownMenuTrigger, -} from '../ui/dropdown-menu'; - -export function ClientActions(client: IServiceClientWithProject) { - const { id } = client; - const router = useRouter(); - const deletion = api.client.remove.useMutation({ - onSuccess() { - toast('Success', { - description: 'Client revoked, incoming requests will be rejected.', - }); - router.refresh(); - }, - }); - return ( - - - - - - Actions - clipboard(id)}> - Copy client ID - - { - pushModal('EditClient', client); - }} - > - Edit - - - { - showConfirm({ - title: 'Revoke client', - text: 'Are you sure you want to revoke this client? This action cannot be undone.', - onConfirm() { - deletion.mutate({ - id, - }); - }, - }); - }} - > - Revoke - - - - ); -} diff --git a/apps/dashboard/src/components/clients/table/columns.tsx b/apps/dashboard/src/components/clients/table/columns.tsx deleted file mode 100644 index fc3b70c8..00000000 --- a/apps/dashboard/src/components/clients/table/columns.tsx +++ /dev/null @@ -1,56 +0,0 @@ -import { EventIcon } from '@/components/events/event-icon'; -import { ProjectLink } from '@/components/links'; -import { SerieIcon } from '@/components/report-chart/common/serie-icon'; -import { TooltipComplete } from '@/components/tooltip-complete'; -import { useNumber } from '@/hooks/useNumerFormatter'; -import { pushModal } from '@/modals'; -import { formatDateTime, formatTime } from '@/utils/date'; -import { getProfileName } from '@/utils/getters'; -import type { ColumnDef } from '@tanstack/react-table'; -import { isToday } from 'date-fns'; - -import { ACTIONS } from '@/components/data-table'; -import type { IServiceClientWithProject, IServiceEvent } from '@openpanel/db'; -import { ClientActions } from '../client-actions'; - -export function useColumns() { - const number = useNumber(); - const columns: ColumnDef[] = [ - { - accessorKey: 'name', - header: 'Name', - cell: ({ row }) => { - return
{row.original.name}
; - }, - }, - { - accessorKey: 'id', - header: 'Client ID', - cell: ({ row }) =>
{row.original.id}
, - }, - // { - // accessorKey: 'secret', - // header: 'Secret', - // cell: (info) => - //
- - // }, - { - accessorKey: 'createdAt', - header: 'Created at', - cell({ row }) { - const date = row.original.createdAt; - return ( -
{isToday(date) ? formatTime(date) : formatDateTime(date)}
- ); - }, - }, - { - id: ACTIONS, - header: 'Actions', - cell: ({ row }) => , - }, - ]; - - return columns; -} diff --git a/apps/dashboard/src/components/clients/table/index.tsx b/apps/dashboard/src/components/clients/table/index.tsx deleted file mode 100644 index e92c1d9e..00000000 --- a/apps/dashboard/src/components/clients/table/index.tsx +++ /dev/null @@ -1,67 +0,0 @@ -import { DataTable } from '@/components/data-table'; -import { FullPageEmptyState } from '@/components/full-page-empty-state'; -import { Pagination } from '@/components/pagination'; -import { Button } from '@/components/ui/button'; -import { TableSkeleton } from '@/components/ui/table'; -import type { UseQueryResult } from '@tanstack/react-query'; -import { GanttChartIcon, PlusIcon } from 'lucide-react'; -import type { Dispatch, SetStateAction } from 'react'; - -import type { IServiceClientWithProject } from '@openpanel/db'; - -import { useAppParams } from '@/hooks/useAppParams'; -import { pushModal } from '@/modals'; -import { useColumns } from './columns'; - -type Props = { - query: UseQueryResult; - cursor: number; - setCursor: Dispatch>; -}; - -export const ClientsTable = ({ query, ...props }: Props) => { - const columns = useColumns(); - const { data, isFetching, isLoading } = query; - - if (isLoading) { - return ; - } - - if (data?.length === 0) { - return ( - -

Could not find any clients

-
- {'cursor' in props && props.cursor !== 0 && ( - - )} - -
-
- ); - } - - return ( - <> - - {'cursor' in props && ( - - )} - - ); -}; diff --git a/apps/dashboard/src/components/dark-mode-toggle.tsx b/apps/dashboard/src/components/dark-mode-toggle.tsx deleted file mode 100644 index 428c6c31..00000000 --- a/apps/dashboard/src/components/dark-mode-toggle.tsx +++ /dev/null @@ -1,43 +0,0 @@ -'use client'; - -import { Button } from '@/components/ui/button'; -import { - DropdownMenu, - DropdownMenuContent, - DropdownMenuItem, - DropdownMenuTrigger, -} from '@/components/ui/dropdown-menu'; -import { MoonIcon, SunIcon } from 'lucide-react'; -import { useTheme } from 'next-themes'; -import * as React from 'react'; - -interface Props { - className?: string; -} - -export default function DarkModeToggle({ className }: Props) { - const { setTheme } = useTheme(); - - return ( - - - - - - setTheme('light')}> - Light - - setTheme('dark')}> - Dark - - {/* setTheme('system')}> - System - */} - - - ); -} diff --git a/apps/dashboard/src/components/data-table.tsx b/apps/dashboard/src/components/data-table.tsx deleted file mode 100644 index c4b106e2..00000000 --- a/apps/dashboard/src/components/data-table.tsx +++ /dev/null @@ -1,120 +0,0 @@ -'use client'; - -import { cn } from '@/utils/cn'; -import { - flexRender, - getCoreRowModel, - useReactTable, -} from '@tanstack/react-table'; -import type { ColumnDef, RowData } from '@tanstack/react-table'; - -import { Grid, GridBody, GridCell, GridHeader, GridRow } from './grid-table'; - -interface DataTableProps { - columns: ColumnDef[]; - data: TData[]; -} - -declare module '@tanstack/react-table' { - // eslint-disable-next-line - interface ColumnMeta { - className?: string; - } -} - -export const ACTIONS = '__actions__'; - -export function TableButtons({ - children, - className, -}: { - children: React.ReactNode; - className?: string; -}) { - return ( -
- {children} -
- ); -} - -export function DataTable({ columns, data }: DataTableProps) { - const table = useReactTable({ - data, - columns, - getCoreRowModel: getCoreRowModel(), - }); - - return ( - - - {table.getHeaderGroups().map((headerGroup) => ( - - {headerGroup.headers.map((header) => { - if (header.column.id === ACTIONS) { - return ( - - Actions - - ); - } - - return ( - - {header.isPlaceholder - ? null - : flexRender( - header.column.columnDef.header, - header.getContext(), - )} - - ); - })} - - ))} - - - {table.getRowModel().rows?.length ? ( - table.getRowModel().rows.map((row) => ( - - {row.getVisibleCells().map((cell) => { - if (cell.column.id === ACTIONS) { - return ( - - {flexRender( - cell.column.columnDef.cell, - cell.getContext(), - )} - - ); - } - - return ( - - {flexRender(cell.column.columnDef.cell, cell.getContext())} - - ); - })} - - )) - ) : ( - - -
No results.
-
-
- )} -
-
- ); -} diff --git a/apps/dashboard/src/components/events/event-field-value.tsx b/apps/dashboard/src/components/events/event-field-value.tsx deleted file mode 100644 index 4d584562..00000000 --- a/apps/dashboard/src/components/events/event-field-value.tsx +++ /dev/null @@ -1,77 +0,0 @@ -import { fancyMinutes } from '@/hooks/useNumerFormatter'; -import { formatDateTime, formatTime } from '@/utils/date'; -import type { IServiceEvent } from '@openpanel/db'; -import { isToday } from 'date-fns'; -import { SerieIcon } from '../report-chart/common/serie-icon'; - -export function EventFieldValue({ - name, - value, - event, -}: { - name: keyof IServiceEvent; - value: any; - event: IServiceEvent; -}) { - if (!value) { - return null; - } - - if (value instanceof Date) { - return isToday(value) ? formatTime(value) : formatDateTime(value); - } - - switch (name) { - case 'osVersion': - return ( -
- - {value} -
- ); - case 'browserVersion': - return ( -
- - {value} -
- ); - case 'city': - return ( -
- - {value} -
- ); - case 'region': - return ( -
- - {value} -
- ); - case 'properties': - return JSON.stringify(value); - case 'country': - case 'browser': - case 'os': - case 'brand': - case 'model': - case 'device': - return ( -
- - {value} -
- ); - case 'duration': - return ( -
- ({value}ms){' '} - {fancyMinutes(value / 1000)} -
- ); - default: - return value; - } -} diff --git a/apps/dashboard/src/components/events/table/events-data-table.tsx b/apps/dashboard/src/components/events/table/events-data-table.tsx deleted file mode 100644 index 9dc2dc3c..00000000 --- a/apps/dashboard/src/components/events/table/events-data-table.tsx +++ /dev/null @@ -1,160 +0,0 @@ -'use client'; - -import { GridCell } from '@/components/grid-table'; -import { cn } from '@/utils/cn'; -import { - flexRender, - getCoreRowModel, - useReactTable, -} from '@tanstack/react-table'; -import type { ColumnDef } from '@tanstack/react-table'; -import { useWindowVirtualizer } from '@tanstack/react-virtual'; -import throttle from 'lodash.throttle'; -import { useEffect, useRef, useState } from 'react'; -import { useEventsTableColumns } from './events-table-columns'; - -interface DataTableProps { - columns: ColumnDef[]; - data: TData[]; -} - -export function EventsDataTable({ - columns, - data, -}: DataTableProps) { - const [visibleColumns] = useEventsTableColumns(); - const table = useReactTable({ - data, - columns, - getCoreRowModel: getCoreRowModel(), - }); - - const parentRef = useRef(null); - const [scrollMargin, setScrollMargin] = useState(0); - const { rows } = table.getRowModel(); - - const virtualizer = useWindowVirtualizer({ - count: rows.length, - estimateSize: () => 48, - scrollMargin, - overscan: 10, - }); - - useEffect(() => { - const updateScrollMargin = throttle(() => { - if (parentRef.current) { - setScrollMargin( - parentRef.current.getBoundingClientRect().top + window.scrollY, - ); - } - }, 500); - - // Initial calculation - updateScrollMargin(); - - // Listen for scroll and resize events - window.addEventListener('scroll', updateScrollMargin); - window.addEventListener('resize', updateScrollMargin); - - return () => { - window.removeEventListener('scroll', updateScrollMargin); - window.removeEventListener('resize', updateScrollMargin); - }; - }, []); // Empty dependency array since we're setting up listeners - - const visibleRows = virtualizer.getVirtualItems(); - - return ( -
-
-
- {table.getHeaderGroups().map((headerGroup) => ( -
- {headerGroup.headers - .filter((header) => visibleColumns.includes(header.id)) - .map((header) => { - return ( - - {header.isPlaceholder - ? null - : flexRender( - header.column.columnDef.header, - header.getContext(), - )} - - ); - })} -
- ))} -
-
- {visibleRows.map((virtualRow, index) => { - const row = rows[virtualRow.index]!; - if (!row) { - return null; - } - - return ( -
- {row - .getVisibleCells() - .filter((cell) => visibleColumns.includes(cell.column.id)) - .map((cell) => { - return ( - - {flexRender( - cell.column.columnDef.cell, - cell.getContext(), - )} - - ); - })} -
- ); - })} -
-
-
-
-
- ); -} diff --git a/apps/dashboard/src/components/events/table/events-table-columns.tsx b/apps/dashboard/src/components/events/table/events-table-columns.tsx deleted file mode 100644 index 8ff858b6..00000000 --- a/apps/dashboard/src/components/events/table/events-table-columns.tsx +++ /dev/null @@ -1,70 +0,0 @@ -import { Button } from '@/components/ui/button'; -import { - DropdownMenu, - DropdownMenuCheckboxItem, - DropdownMenuContent, - DropdownMenuLabel, - DropdownMenuSeparator, - DropdownMenuTrigger, -} from '@/components/ui/dropdown-menu'; -import { ColumnsIcon } from 'lucide-react'; -import { useQueryState } from 'nuqs'; -import { useLocalStorage } from 'usehooks-ts'; - -// Define available columns -const AVAILABLE_COLUMNS = [ - { id: 'name', label: 'Name' }, - { id: 'createdAt', label: 'Created at' }, - { id: 'profileId', label: 'Profile' }, - { id: 'country', label: 'Country' }, - { id: 'os', label: 'OS' }, - { id: 'browser', label: 'Browser' }, - { id: 'properties', label: 'Properties' }, - { id: 'sessionId', label: 'Session ID' }, - { id: 'deviceId', label: 'Device ID' }, -] as const; - -export function useEventsTableColumns() { - return useLocalStorage('@op:events-table-columns', [ - 'name', - 'createdAt', - 'profileId', - 'country', - 'os', - 'browser', - ]); -} - -export function EventsTableColumns() { - const [visibleColumns, setVisibleColumns] = useEventsTableColumns(); - - return ( - - - - - - Toggle columns - - {AVAILABLE_COLUMNS.map((column) => ( - { - setVisibleColumns( - checked - ? [...visibleColumns, column.id] - : visibleColumns.filter((id) => id !== column.id), - ); - }} - > - {column.label} - - ))} - - - ); -} diff --git a/apps/dashboard/src/components/events/table/index.tsx b/apps/dashboard/src/components/events/table/index.tsx deleted file mode 100644 index d37b0f6d..00000000 --- a/apps/dashboard/src/components/events/table/index.tsx +++ /dev/null @@ -1,128 +0,0 @@ -import { FullPageEmptyState } from '@/components/full-page-empty-state'; -import { TableSkeleton } from '@/components/ui/table'; -import type { - UseInfiniteQueryResult, - UseQueryResult, -} from '@tanstack/react-query'; -import { GanttChartIcon, Loader2Icon } from 'lucide-react'; -import { useEffect, useRef, useState } from 'react'; - -import { pushModal, replaceWithModal, useOnPushModal } from '@/modals'; -import type { RouterOutputs } from '@/trpc/client'; -import { cn } from '@/utils/cn'; -import { shouldIgnoreKeypress } from '@/utils/should-ignore-keypress'; -import { bind } from 'bind-event-listener'; -import { useInViewport } from 'react-in-viewport'; -import { useColumns } from './columns'; -import { EventsDataTable } from './events-data-table'; - -type Props = - | { - query: UseInfiniteQueryResult; - } - | { - query: UseQueryResult; - }; - -export const EventsTable = ({ query, ...props }: Props) => { - const columns = useColumns(); - const { isLoading } = query; - const ref = useRef(null); - const { inViewport, enterCount } = useInViewport(ref, undefined, { - disconnectOnLeave: true, - }); - const isInfiniteQuery = 'fetchNextPage' in query; - const data = - (isInfiniteQuery - ? query.data?.pages?.flatMap((p) => p.items) - : query.data?.items) ?? []; - - const hasNextPage = isInfiniteQuery - ? query.data?.pages[query.data.pages.length - 1]?.meta.next - : query.data?.meta.next; - - const [eventId, setEventId] = useState(null); - useOnPushModal('EventDetails', (isOpen, props) => { - setEventId(isOpen ? props.id : null); - }); - - useEffect(() => { - return bind(window, { - type: 'keydown', - listener(event) { - if (shouldIgnoreKeypress(event)) { - return; - } - - if (event.key === 'ArrowLeft') { - const index = data.findIndex((p) => p.id === eventId); - if (index !== -1) { - const match = data[index - 1]; - if (match) { - replaceWithModal('EventDetails', match); - } - } - } - if (event.key === 'ArrowRight') { - const index = data.findIndex((p) => p.id === eventId); - if (index !== -1) { - const match = data[index + 1]; - if (match) { - replaceWithModal('EventDetails', match); - } else if ( - hasNextPage && - isInfiniteQuery && - data.length > 0 && - query.isFetchingNextPage === false - ) { - query.fetchNextPage(); - } - } - } - }, - }); - }, [data, eventId]); - - useEffect(() => { - if ( - hasNextPage && - isInfiniteQuery && - data.length > 0 && - inViewport && - enterCount > 0 && - query.isFetchingNextPage === false - ) { - query.fetchNextPage(); - } - }, [inViewport, enterCount, hasNextPage]); - - if (isLoading) { - return ; - } - - if (data.length === 0) { - return ( - -

Could not find any events

-
- ); - } - - return ( - <> - - {isInfiniteQuery && ( -
-
- -
-
- )} - - ); -}; diff --git a/apps/dashboard/src/components/links.tsx b/apps/dashboard/src/components/links.tsx deleted file mode 100644 index bb294943..00000000 --- a/apps/dashboard/src/components/links.tsx +++ /dev/null @@ -1,30 +0,0 @@ -import { useAppParams } from '@/hooks/useAppParams'; -import type { LinkProps } from 'next/link'; -import Link from 'next/link'; - -export function ProjectLink({ - children, - ...props -}: LinkProps & { - children: React.ReactNode; - className?: string; - title?: string; -}) { - const { organizationId, projectId } = useAppParams(); - if (typeof props.href === 'string') { - return ( - - {children} - - ); - } - - return

ProjectLink

; -} diff --git a/apps/dashboard/src/components/notifications/notification-provider.tsx b/apps/dashboard/src/components/notifications/notification-provider.tsx deleted file mode 100644 index fb50a6c2..00000000 --- a/apps/dashboard/src/components/notifications/notification-provider.tsx +++ /dev/null @@ -1,29 +0,0 @@ -import { useAppParams } from '@/hooks/useAppParams'; -import useWS from '@/hooks/useWS'; -import type { Prisma } from '@openpanel/db'; -import { BellIcon } from 'lucide-react'; -import { toast } from 'sonner'; - -export function NotificationProvider() { - const { projectId } = useAppParams(); - - if (!projectId) return null; - - return ; -} - -export function InnerNotificationProvider({ - projectId, -}: { projectId: string }) { - useWS( - `/live/notifications/${projectId}`, - (notification) => { - toast(notification.title, { - description: notification.message, - icon: , - }); - }, - ); - - return null; -} diff --git a/apps/dashboard/src/components/notifications/notifications.tsx b/apps/dashboard/src/components/notifications/notifications.tsx deleted file mode 100644 index 82296022..00000000 --- a/apps/dashboard/src/components/notifications/notifications.tsx +++ /dev/null @@ -1,14 +0,0 @@ -'use client'; - -import { useAppParams } from '@/hooks/useAppParams'; -import { api } from '@/trpc/client'; -import { NotificationsTable } from './table'; - -export function Notifications() { - const { projectId } = useAppParams(); - const query = api.notification.list.useQuery({ - projectId, - }); - - return ; -} diff --git a/apps/dashboard/src/components/notifications/table/index.tsx b/apps/dashboard/src/components/notifications/table/index.tsx deleted file mode 100644 index 1c44f8b5..00000000 --- a/apps/dashboard/src/components/notifications/table/index.tsx +++ /dev/null @@ -1,63 +0,0 @@ -import { DataTable } from '@/components/data-table'; -import { FullPageEmptyState } from '@/components/full-page-empty-state'; -import { Pagination } from '@/components/pagination'; -import { Button } from '@/components/ui/button'; -import { TableSkeleton } from '@/components/ui/table'; -import type { UseQueryResult } from '@tanstack/react-query'; -import { GanttChartIcon } from 'lucide-react'; -import type { Dispatch, SetStateAction } from 'react'; - -import type { RouterOutputs } from '@/trpc/client'; -import { useColumns } from './columns'; - -type Props = - | { - query: UseQueryResult; - } - | { - query: UseQueryResult; - cursor: number; - setCursor: Dispatch>; - }; - -export const NotificationsTable = ({ query, ...props }: Props) => { - const columns = useColumns(); - const { data, isFetching, isLoading } = query; - - if (isLoading) { - return ; - } - - if (data?.length === 0) { - return ( - -

Could not find any notifications

- {'cursor' in props && props.cursor !== 0 && ( - - )} -
- ); - } - - return ( - <> - - {'cursor' in props && ( - - )} - - ); -}; diff --git a/apps/dashboard/src/components/overview/live-counter/index.tsx b/apps/dashboard/src/components/overview/live-counter/index.tsx deleted file mode 100644 index 4ee4473c..00000000 --- a/apps/dashboard/src/components/overview/live-counter/index.tsx +++ /dev/null @@ -1,13 +0,0 @@ -import withSuspense from '@/hocs/with-suspense'; - -import { eventBuffer } from '@openpanel/db'; - -import type { LiveCounterProps } from './live-counter'; -import LiveCounter from './live-counter'; - -async function ServerLiveCounter(props: Omit) { - const count = await eventBuffer.getActiveVisitorCount(props.projectId); - return ; -} - -export default withSuspense(ServerLiveCounter, () =>
); diff --git a/apps/dashboard/src/components/overview/live-counter/live-counter.tsx b/apps/dashboard/src/components/overview/live-counter/live-counter.tsx deleted file mode 100644 index d03d91c2..00000000 --- a/apps/dashboard/src/components/overview/live-counter/live-counter.tsx +++ /dev/null @@ -1,85 +0,0 @@ -'use client'; - -import { TooltipComplete } from '@/components/tooltip-complete'; -import { - Tooltip, - TooltipContent, - TooltipTrigger, -} from '@/components/ui/tooltip'; -import { useDebounceState } from '@/hooks/useDebounceState'; -import useWS from '@/hooks/useWS'; -import { cn } from '@/utils/cn'; -import { useQueryClient } from '@tanstack/react-query'; -import dynamic from 'next/dynamic'; -import { useRef, useState } from 'react'; -import { toast } from 'sonner'; - -export interface LiveCounterProps { - data: number; - projectId: string; -} - -const AnimatedNumbers = dynamic(() => import('react-animated-numbers'), { - ssr: false, - loading: () =>
0
, -}); - -const FIFTEEN_SECONDS = 1000 * 30; - -export default function LiveCounter({ data = 0, projectId }: LiveCounterProps) { - const client = useQueryClient(); - const counter = useDebounceState(data, 1000, { - maxWait: 5000, - }); - const lastRefresh = useRef(Date.now()); - - useWS(`/live/visitors/${projectId}`, (value) => { - if (!Number.isNaN(value)) { - counter.set(value); - if (Date.now() - lastRefresh.current > FIFTEEN_SECONDS) { - lastRefresh.current = Date.now(); - if (!document.hidden) { - toast('Refreshed data'); - client.refetchQueries({ - type: 'active', - }); - } - } - } - }); - - return ( - -
-
-
-
-
- ({ - type: 'spring', - duration: index + 0.3, - - damping: 10, - stiffness: 200, - })} - animateToNumber={counter.debounced} - locale="en" - /> -
- - ); -} diff --git a/apps/dashboard/src/components/overview/overview-hydrate-options.tsx b/apps/dashboard/src/components/overview/overview-hydrate-options.tsx deleted file mode 100644 index fe871aaf..00000000 --- a/apps/dashboard/src/components/overview/overview-hydrate-options.tsx +++ /dev/null @@ -1,22 +0,0 @@ -'use client'; - -import { getStorageItem } from '@/utils/storage'; -import { useEffect, useRef } from 'react'; -import { useOverviewOptions } from './useOverviewOptions'; - -export function OverviewHydrateOptions() { - const { setRange, range } = useOverviewOptions(); - const ref = useRef(false); - - useEffect(() => { - if (!ref.current) { - const range = getStorageItem('range', '7d'); - if (range !== '7d') { - setRange(range); - } - ref.current = true; - } - }, []); - - return null; -} diff --git a/apps/dashboard/src/components/overview/overview-live-histogram.tsx b/apps/dashboard/src/components/overview/overview-live-histogram.tsx deleted file mode 100644 index 2f9c06ef..00000000 --- a/apps/dashboard/src/components/overview/overview-live-histogram.tsx +++ /dev/null @@ -1,140 +0,0 @@ -'use client'; - -import { api } from '@/trpc/client'; -import { cn } from '@/utils/cn'; - -import type { IChartProps } from '@openpanel/validation'; - -import { Tooltip, TooltipContent, TooltipTrigger } from '../ui/tooltip'; - -interface OverviewLiveHistogramProps { - projectId: string; -} - -export function OverviewLiveHistogram({ - projectId, -}: OverviewLiveHistogramProps) { - const report: IChartProps = { - projectId, - events: [ - { - segment: 'user', - filters: [ - { - id: '1', - name: 'name', - operator: 'is', - value: ['screen_view', 'session_start'], - }, - ], - id: 'A', - name: '*', - displayName: 'Active users', - }, - ], - chartType: 'histogram', - interval: 'minute', - range: '30min', - name: '', - metric: 'sum', - breakdowns: [], - lineType: 'monotone', - previous: false, - }; - const countReport: IChartProps = { - name: '', - projectId, - events: [ - { - segment: 'user', - filters: [], - id: 'A', - name: 'session_start', - }, - ], - breakdowns: [], - chartType: 'metric', - lineType: 'monotone', - interval: 'minute', - range: '30min', - previous: false, - metric: 'sum', - }; - - const res = api.chart.chart.useQuery(report); - const countRes = api.chart.chart.useQuery(countReport); - - const metrics = res.data?.series[0]?.metrics; - const minutes = (res.data?.series[0]?.data || []).slice(-30); - const liveCount = countRes.data?.series[0]?.metrics?.sum ?? 0; - - if (res.isInitialLoading || countRes.isInitialLoading) { - const staticArray = [ - 10, 25, 30, 45, 20, 5, 55, 18, 40, 12, 50, 35, 8, 22, 38, 42, 15, 28, 52, - 5, 48, 14, 32, 58, 7, 19, 33, 56, 24, 5, - ]; - - return ( - - {staticArray.map((percent, i) => ( -
- ))} - - ); - } - - if (!res.isSuccess && !countRes.isSuccess) { - return null; - } - - return ( - - {minutes.map((minute) => { - return ( - - -
- - -
{minute.count} active users
-
@ {new Date(minute.date).toLocaleTimeString()}
-
- - ); - })} - - ); -} - -interface WrapperProps { - children: React.ReactNode; - count: number; -} - -function Wrapper({ children, count }: WrapperProps) { - return ( -
-
- {count} unique vistors last 30 minutes -
-
- {children} -
-
- ); -} diff --git a/apps/dashboard/src/components/overview/overview-share/index.tsx b/apps/dashboard/src/components/overview/overview-share/index.tsx deleted file mode 100644 index e962b96a..00000000 --- a/apps/dashboard/src/components/overview/overview-share/index.tsx +++ /dev/null @@ -1,19 +0,0 @@ -import { Button } from '@/components/ui/button'; -import withSuspense from '@/hocs/with-suspense'; - -import { getShareByProjectId } from '@openpanel/db'; - -import { OverviewShare } from './overview-share'; - -type Props = { - projectId: string; -}; - -const OverviewShareServer = async ({ projectId }: Props) => { - const share = await getShareByProjectId(projectId); - return ; -}; - -export default withSuspense(OverviewShareServer, () => ( - -)); diff --git a/apps/dashboard/src/components/overview/overview-top-bots.tsx b/apps/dashboard/src/components/overview/overview-top-bots.tsx deleted file mode 100644 index c273ebec..00000000 --- a/apps/dashboard/src/components/overview/overview-top-bots.tsx +++ /dev/null @@ -1,80 +0,0 @@ -import { api } from '@/trpc/client'; -import { useState } from 'react'; - -import { Pagination } from '../pagination'; -import { Tooltiper } from '../ui/tooltip'; -import { WidgetTable } from '../widget-table'; - -interface Props { - projectId: string; -} - -function getPath(path: string) { - try { - return new URL(path).pathname; - } catch { - return path; - } -} - -const OverviewTopBots = ({ projectId }: Props) => { - const [cursor, setCursor] = useState(0); - const res = api.event.bots.useQuery( - { projectId, cursor }, - { - keepPreviousData: true, - }, - ); - const data = res.data?.data ?? []; - const count = res.data?.count ?? 0; - - return ( - <> - item.id} - columns={[ - { - name: 'Path', - width: 'w-full', - render(item) { - return ( - - {getPath(item.path)} - - ); - }, - }, - { - name: 'Date', - width: '100px', - render(item) { - return ( -
- -
{item.name}
-
- -
{item.createdAt.toLocaleDateString()}
-
-
- ); - }, - }, - ]} - /> - - - ); -}; - -export default OverviewTopBots; diff --git a/apps/dashboard/src/components/overview/overview-top-events/index.tsx b/apps/dashboard/src/components/overview/overview-top-events/index.tsx deleted file mode 100644 index 6c9601c1..00000000 --- a/apps/dashboard/src/components/overview/overview-top-events/index.tsx +++ /dev/null @@ -1,16 +0,0 @@ -import { getConversionEventNames } from '@openpanel/db'; - -import type { OverviewTopEventsProps } from './overview-top-events'; -import OverviewTopEvents from './overview-top-events'; - -export default async function OverviewTopEventsServer({ - projectId, -}: Omit) { - const eventNames = await getConversionEventNames(projectId); - return ( - item.name)} - /> - ); -} diff --git a/apps/dashboard/src/components/page-tabs.tsx b/apps/dashboard/src/components/page-tabs.tsx deleted file mode 100644 index a17e9b64..00000000 --- a/apps/dashboard/src/components/page-tabs.tsx +++ /dev/null @@ -1,75 +0,0 @@ -'use client'; - -import { cn } from '@/utils/cn'; -import { motion } from 'framer-motion'; -import Link from 'next/link'; -import { useState } from 'react'; - -export function PageTabs({ - children, - className, -}: { - children: React.ReactNode; - className?: string; -}) { - return ( -
-
- {children} -
-
- ); -} - -export function PageTabsLink({ - href, - children, - isActive = false, -}: { - href: string; - children: React.ReactNode; - isActive?: boolean; -}) { - return ( -
- - {children} - - {isActive && ( - - )} -
- ); -} - -export function PageTabsItem({ - onClick, - children, - isActive = false, -}: { - onClick: () => void; - children: React.ReactNode; - isActive?: boolean; -}) { - return ( - - ); -} diff --git a/apps/dashboard/src/components/pagination.tsx b/apps/dashboard/src/components/pagination.tsx deleted file mode 100644 index 35a254b1..00000000 --- a/apps/dashboard/src/components/pagination.tsx +++ /dev/null @@ -1,82 +0,0 @@ -import { cn } from '@/utils/cn'; -import { - ChevronLeftIcon, - ChevronRightIcon, - ChevronsLeftIcon, - ChevronsRightIcon, -} from 'lucide-react'; -import type { Dispatch, SetStateAction } from 'react'; -import { useState } from 'react'; - -import { Button } from './ui/button'; - -export function usePagination(take: number) { - const [page, setPage] = useState(0); - return { - take, - skip: page * take, - setPage, - page, - paginate: (data: T[]): T[] => - data.slice(page * take, (page + 1) * take), - }; -} - -export function Pagination({ - take, - count, - cursor, - setCursor, - className, - size = 'base', - loading, -}: { - take: number; - count: number; - cursor: number; - setCursor: Dispatch>; - className?: string; - size?: 'sm' | 'base'; - loading?: boolean; -}) { - const lastCursor = Math.floor(count / take) - 1; - const isNextDisabled = count === 0 || lastCursor === cursor; - return ( -
- {size === 'base' && ( - - -
- ); -} diff --git a/apps/dashboard/src/components/profiles/table/index.tsx b/apps/dashboard/src/components/profiles/table/index.tsx deleted file mode 100644 index b98e3e76..00000000 --- a/apps/dashboard/src/components/profiles/table/index.tsx +++ /dev/null @@ -1,74 +0,0 @@ -import { DataTable } from '@/components/data-table'; -import { FullPageEmptyState } from '@/components/full-page-empty-state'; -import { Pagination } from '@/components/pagination'; -import { Button } from '@/components/ui/button'; -import { TableSkeleton } from '@/components/ui/table'; -import type { UseQueryResult } from '@tanstack/react-query'; -import isEqual from 'lodash.isequal'; -import { GanttChartIcon } from 'lucide-react'; -import { memo } from 'react'; -import type { Dispatch, SetStateAction } from 'react'; - -import type { IServiceProfile } from '@openpanel/db'; - -import { useColumns } from './columns'; - -type CommonProps = { - type?: 'profiles' | 'power-users'; - query: UseQueryResult; -}; -type Props = - | CommonProps - | (CommonProps & { - cursor: number; - setCursor: Dispatch>; - }); - -export const ProfilesTable = memo( - ({ type, query, ...props }: Props) => { - const columns = useColumns(type); - const { data, isFetching, isLoading } = query; - - if (isLoading) { - return ; - } - - if (data?.length === 0) { - return ( - -

Could not find any profiles

- {'cursor' in props && props.cursor !== 0 && ( - - )} -
- ); - } - - return ( - <> - - {'cursor' in props && ( - - )} - - ); - }, - (prevProps, nextProps) => { - return isEqual(prevProps.query.data, nextProps.query.data); - }, -); - -ProfilesTable.displayName = 'ProfilesTable'; diff --git a/apps/dashboard/src/components/projects/project-card.tsx b/apps/dashboard/src/components/projects/project-card.tsx deleted file mode 100644 index 8953aa05..00000000 --- a/apps/dashboard/src/components/projects/project-card.tsx +++ /dev/null @@ -1,114 +0,0 @@ -import { shortNumber } from '@/hooks/useNumerFormatter'; -import { Suspense } from 'react'; -import { escape } from 'sqlstring'; - -import type { IServiceProject } from '@openpanel/db'; -import { TABLE_NAMES, chQuery } from '@openpanel/db'; - -import { SettingsIcon } from 'lucide-react'; -import Link from 'next/link'; -import { ChartSSR } from '../chart-ssr'; -import { FadeIn } from '../fade-in'; -import { SerieIcon } from '../report-chart/common/serie-icon'; -import { LinkButton } from '../ui/button'; - -function ProjectCard({ id, domain, name, organizationId }: IServiceProject) { - // For some unknown reason I get when navigating back to this page when using - // Should be solved: https://github.com/vercel/next.js/issues/61336 - // But still get the error - return ( - - ); -} - -async function ProjectChart({ id }: { id: string }) { - const chart = await chQuery<{ value: number; date: string }>( - `SELECT - uniqHLL12(profile_id) as value, - toStartOfDay(created_at) as date - FROM ${TABLE_NAMES.sessions} - WHERE - sign = 1 AND - project_id = ${escape(id)} AND - created_at >= now() - interval '1 month' - GROUP BY date - ORDER BY date ASC - WITH FILL FROM toStartOfDay(now() - interval '1 month') - TO toStartOfDay(now()) - STEP INTERVAL 1 day - `, - ); - - return ( - - ({ ...d, date: new Date(d.date) }))} /> - - ); -} - -function Metric({ value, label }: { value: React.ReactNode; label: string }) { - return ( -
-
{label}
- {value} -
- ); -} - -async function ProjectMetrics({ id }: { id: string }) { - const [metrics] = await chQuery<{ - months_3: number; - month: number; - day: number; - }>( - ` - SELECT - uniqHLL12(if(created_at >= (now() - toIntervalMonth(6)), profile_id, null)) AS months_3, - uniqHLL12(if(created_at >= (now() - toIntervalMonth(1)), profile_id, null)) AS month, - uniqHLL12(if(created_at >= (now() - toIntervalDay(1)), profile_id, null)) AS day -FROM sessions -WHERE - project_id = ${escape(id)} AND - created_at >= (now() - toIntervalMonth(6)) - `, - ); - - return ( - - - - - - ); -} - -export default ProjectCard; diff --git a/apps/dashboard/src/components/references/table.tsx b/apps/dashboard/src/components/references/table.tsx deleted file mode 100644 index 175617d9..00000000 --- a/apps/dashboard/src/components/references/table.tsx +++ /dev/null @@ -1,27 +0,0 @@ -import { formatDate, formatDateTime } from '@/utils/date'; -import type { ColumnDef } from '@tanstack/react-table'; - -import type { IServiceReference } from '@openpanel/db'; - -export const columns: ColumnDef[] = [ - { - accessorKey: 'title', - header: 'Title', - }, - { - accessorKey: 'date', - header: 'Date', - cell({ row }) { - const date = row.original.date; - return
{formatDateTime(date)}
; - }, - }, - { - accessorKey: 'createdAt', - header: 'Created at', - cell({ row }) { - const date = row.original.createdAt; - return
{formatDate(date)}
; - }, - }, -]; diff --git a/apps/dashboard/src/components/settings-toggle.tsx b/apps/dashboard/src/components/settings-toggle.tsx deleted file mode 100644 index 2bf8a7fd..00000000 --- a/apps/dashboard/src/components/settings-toggle.tsx +++ /dev/null @@ -1,113 +0,0 @@ -'use client'; - -import { Button } from '@/components/ui/button'; -import { - DropdownMenu, - DropdownMenuContent, - DropdownMenuItem, - DropdownMenuLabel, - DropdownMenuSeparator, - DropdownMenuShortcut, - DropdownMenuSub, - DropdownMenuSubContent, - DropdownMenuSubTrigger, - DropdownMenuTrigger, -} from '@/components/ui/dropdown-menu'; -import { CheckIcon, MoreHorizontalIcon, PlusIcon } from 'lucide-react'; -import { useTheme } from 'next-themes'; -import * as React from 'react'; - -import { useAppParams } from '@/hooks/useAppParams'; -import { useLogout } from '@/hooks/useLogout'; -import { api } from '@/trpc/client'; -import { useRouter } from 'next/navigation'; -import { ProjectLink } from './links'; - -interface Props { - className?: string; -} - -export default function SettingsToggle({ className }: Props) { - const router = useRouter(); - const { setTheme, theme } = useTheme(); - const { projectId } = useAppParams(); - const logout = useLogout(); - - return ( - - - - - - {projectId && ( - <> - - - Create report - - - - - - - Settings - - - Organization - - - - - Project & Clients - - - - Your profile - - - References - - - - Notifications - - - - - Integrations - - - - - )} - - - Theme - {theme} - - - {['system', 'light', 'dark'].map((themeOption) => ( - setTheme(themeOption)} - className="capitalize" - > - {themeOption} - {theme === themeOption && ( - - )} - - ))} - - - - logout()}> - Logout - - - - ); -} diff --git a/apps/dashboard/src/components/settings/invites/columns.tsx b/apps/dashboard/src/components/settings/invites/columns.tsx deleted file mode 100644 index 2dbe319f..00000000 --- a/apps/dashboard/src/components/settings/invites/columns.tsx +++ /dev/null @@ -1,126 +0,0 @@ -import { TooltipComplete } from '@/components/tooltip-complete'; -import { Badge } from '@/components/ui/badge'; -import { Button } from '@/components/ui/button'; -import { - DropdownMenu, - DropdownMenuContent, - DropdownMenuItem, - DropdownMenuTrigger, -} from '@/components/ui/dropdown-menu'; -import { api } from '@/trpc/client'; -import type { ColumnDef, Row } from '@tanstack/react-table'; -import { MoreHorizontalIcon } from 'lucide-react'; -import { useRouter } from 'next/navigation'; -import { pathOr } from 'ramda'; -import { toast } from 'sonner'; - -import { ACTIONS } from '@/components/data-table'; -import { clipboard } from '@/utils/clipboard'; -import type { IServiceInvite, IServiceProject } from '@openpanel/db'; - -export function useColumns( - projects: IServiceProject[], -): ColumnDef[] { - return [ - { - accessorKey: 'id', - }, - { - accessorKey: 'email', - header: 'Mail', - cell: ({ row }) => ( -
{row.original.email}
- ), - }, - { - accessorKey: 'role', - header: 'Role', - }, - { - accessorKey: 'createdAt', - header: 'Created', - cell: ({ row }) => ( - - {new Date(row.original.createdAt).toLocaleDateString()} - - ), - }, - { - accessorKey: 'projectAccess', - header: 'Access', - cell: ({ row }) => { - const access = row.original.projectAccess; - return ( - <> - {access.map((id) => { - const project = projects.find((p) => p.id === id); - if (!project) { - return ( - - Unknown - - ); - } - return ( - - {project.name} - - ); - })} - {access.length === 0 && ( - All projects - )} - - ); - }, - }, - { - id: ACTIONS, - cell: ({ row }) => { - return ; - }, - }, - ]; -} - -function ActionCell({ row }: { row: Row }) { - const router = useRouter(); - const revoke = api.organization.revokeInvite.useMutation({ - onSuccess() { - toast.success(`Invite for ${row.original.email} revoked`); - router.refresh(); - }, - onError() { - toast.error(`Failed to revoke invite for ${row.original.email}`); - }, - }); - - return ( - - - - ); -}; - -export default SignOutButton; diff --git a/apps/dashboard/src/components/ui/badge.tsx b/apps/dashboard/src/components/ui/badge.tsx deleted file mode 100644 index 0c56a752..00000000 --- a/apps/dashboard/src/components/ui/badge.tsx +++ /dev/null @@ -1,42 +0,0 @@ -'use client'; - -import { cn } from '@/utils/cn'; -import { cva } from 'class-variance-authority'; -import type { VariantProps } from 'class-variance-authority'; -import type * as React from 'react'; - -const badgeVariants = cva( - 'inline-flex h-[20px] items-center rounded border px-2 text-sm font-mono transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2', - { - variants: { - variant: { - default: - 'border-transparent bg-primary text-primary-foreground hover:bg-primary/80', - secondary: - 'border-transparent bg-def-100 text-secondary-foreground hover:bg-def-100/80', - destructive: - 'border-transparent bg-destructive-foreground text-destructive hover:bg-destructive/80', - success: - 'border-transparent bg-emerald-500 text-emerald-100 hover:bg-emerald-500/80', - outline: 'text-foreground', - muted: 'bg-def-100 text-foreground', - foregroundish: 'bg-foregroundish text-foregroundish-foreground', - }, - }, - defaultVariants: { - variant: 'default', - }, - }, -); - -export interface BadgeProps - extends React.HTMLAttributes, - VariantProps {} - -function Badge({ className, variant, ...props }: BadgeProps) { - return ( -
- ); -} - -export { Badge, badgeVariants }; diff --git a/apps/dashboard/src/components/ui/button.tsx b/apps/dashboard/src/components/ui/button.tsx deleted file mode 100644 index 5e85f4ea..00000000 --- a/apps/dashboard/src/components/ui/button.tsx +++ /dev/null @@ -1,180 +0,0 @@ -'use client'; - -import { cn } from '@/utils/cn'; -import { Slot } from '@radix-ui/react-slot'; -import { cva } from 'class-variance-authority'; -import type { VariantProps } from 'class-variance-authority'; -import type { LucideIcon } from 'lucide-react'; -import { Loader2 } from 'lucide-react'; -import Link from 'next/link'; -import * as React from 'react'; - -const buttonVariants = cva( - 'inline-flex flex-shrink-0 select-none items-center justify-center whitespace-nowrap rounded-md font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 hover:translate-y-[-1px]', - { - variants: { - variant: { - default: 'bg-primary text-primary-foreground hover:bg-primary/90', - cta: 'bg-highlight text-white hover:bg-highlight/80', - destructive: - 'bg-destructive text-destructive-foreground hover:bg-destructive/90', - outline: - 'border border-input bg-card hover:bg-accent hover:text-accent-foreground', - secondary: 'bg-def-100 text-secondary-foreground hover:bg-def-100/80', - ghost: 'hover:bg-accent hover:text-accent-foreground', - link: 'text-primary underline-offset-4 hover:underline', - }, - size: { - default: 'h-10 px-4 py-2', - sm: 'h-8 rounded-md px-2', - lg: 'h-11 rounded-md px-8', - icon: 'h-8 w-8', - }, - }, - defaultVariants: { - variant: 'default', - size: 'sm', - }, - }, -); - -export interface ButtonProps - extends React.ButtonHTMLAttributes, - VariantProps { - asChild?: boolean; - loading?: boolean; - icon?: LucideIcon; - responsive?: boolean; - autoHeight?: boolean; -} - -function fixHeight({ - autoHeight, - size, -}: { autoHeight?: boolean; size: ButtonProps['size'] }) { - if (autoHeight) { - switch (size) { - case 'lg': - return 'h-auto min-h-11 py-2'; - case 'icon': - return 'h-auto min-h-8 py-1'; - case 'default': - return 'h-auto min-h-10 py-2'; - default: - return 'h-auto min-h-8 py-1'; - } - } - return ''; -} - -const Button = React.forwardRef( - ( - { - className, - variant, - size, - asChild = false, - children, - loading, - disabled, - icon, - responsive, - autoHeight, - ...props - }, - ref, - ) => { - const Comp = asChild ? Slot : 'button'; - const Icon = loading ? Loader2 : (icon ?? null); - return ( - - {Icon && ( - - )} - {responsive ? ( - {children} - ) : ( - children - )} - - ); - }, -); -Button.displayName = 'Button'; -Button.defaultProps = { - type: 'button', -}; -export interface LinkButtonProps - extends React.AnchorHTMLAttributes, - VariantProps { - asChild?: boolean; - loading?: boolean; - icon?: LucideIcon; - responsive?: boolean; - href: string; - prefetch?: boolean; -} - -const LinkButton = React.forwardRef< - typeof Link, - React.PropsWithoutRef ->( - ( - { - className, - variant, - size, - children, - loading, - icon, - responsive, - href, - ...props - }, - ref, - ) => { - const Icon = loading ? Loader2 : (icon ?? null); - return ( - - {Icon && ( - - )} - {responsive ? ( - {children} - ) : ( - children - )} - - ); - }, -); -LinkButton.displayName = 'LinkButton'; - -export { Button, LinkButton, buttonVariants }; diff --git a/apps/dashboard/src/components/ui/calendar.tsx b/apps/dashboard/src/components/ui/calendar.tsx deleted file mode 100644 index 5f07ea35..00000000 --- a/apps/dashboard/src/components/ui/calendar.tsx +++ /dev/null @@ -1,63 +0,0 @@ -import { buttonVariants } from '@/components/ui/button'; -import { cn } from '@/utils/cn'; -import { ChevronLeft, ChevronRight } from 'lucide-react'; -import type * as React from 'react'; -import { DayPicker } from 'react-day-picker'; - -export type CalendarProps = React.ComponentProps; - -function Calendar({ - className, - classNames, - showOutsideDays = true, - ...props -}: CalendarProps) { - return ( - , - IconRight: ({ ...props }) => , - }} - {...props} - /> - ); -} -Calendar.displayName = 'Calendar'; - -export { Calendar }; diff --git a/apps/dashboard/src/components/ui/command.tsx b/apps/dashboard/src/components/ui/command.tsx deleted file mode 100644 index 8c7d2b81..00000000 --- a/apps/dashboard/src/components/ui/command.tsx +++ /dev/null @@ -1,151 +0,0 @@ -'use client'; - -import { Dialog, DialogContent } from '@/components/ui/dialog'; -import { cn } from '@/utils/cn'; -import type { DialogProps } from '@radix-ui/react-dialog'; -import { Command as CommandPrimitive } from 'cmdk'; -import { Search } from 'lucide-react'; -import * as React from 'react'; - -const Command = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className, ...props }, ref) => ( - -)); -Command.displayName = CommandPrimitive.displayName; - -type CommandDialogProps = DialogProps; - -const CommandDialog = ({ children, ...props }: CommandDialogProps) => { - return ( - - - - {children} - - - - ); -}; - -const CommandInput = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className, ...props }, ref) => ( -
- - -
-)); - -CommandInput.displayName = CommandPrimitive.Input.displayName; - -const CommandList = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className, ...props }, ref) => ( - -)); - -CommandList.displayName = CommandPrimitive.List.displayName; - -const CommandEmpty = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef ->((props, ref) => ( - -)); - -CommandEmpty.displayName = CommandPrimitive.Empty.displayName; - -const CommandGroup = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className, ...props }, ref) => ( - -)); - -CommandGroup.displayName = CommandPrimitive.Group.displayName; - -const CommandSeparator = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className, ...props }, ref) => ( - -)); -CommandSeparator.displayName = CommandPrimitive.Separator.displayName; - -const CommandItem = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className, ...props }, ref) => ( - -)); - -CommandItem.displayName = CommandPrimitive.Item.displayName; - -const CommandShortcut = ({ - className, - ...props -}: React.HTMLAttributes) => { - return ( - - ); -}; -CommandShortcut.displayName = 'CommandShortcut'; - -export { - Command, - CommandDialog, - CommandInput, - CommandList, - CommandEmpty, - CommandGroup, - CommandItem, - CommandShortcut, - CommandSeparator, -}; diff --git a/apps/dashboard/src/components/ui/dialog.tsx b/apps/dashboard/src/components/ui/dialog.tsx deleted file mode 100644 index bda48607..00000000 --- a/apps/dashboard/src/components/ui/dialog.tsx +++ /dev/null @@ -1,120 +0,0 @@ -'use client'; - -import { cn } from '@/utils/cn'; -import * as DialogPrimitive from '@radix-ui/react-dialog'; -import { X } from 'lucide-react'; -import * as React from 'react'; - -const Dialog = DialogPrimitive.Root; - -const DialogTrigger = DialogPrimitive.Trigger; - -const DialogPortal = DialogPrimitive.Portal; - -const DialogClose = DialogPrimitive.Close; - -const DialogOverlay = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className, ...props }, ref) => ( - -)); -DialogOverlay.displayName = DialogPrimitive.Overlay.displayName; - -const DialogContent = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef & { - onClose?: () => void; - } ->(({ className, children, onClose, ...props }, ref) => ( - - - - {children} - - -)); -DialogContent.displayName = DialogPrimitive.Content.displayName; - -const DialogHeader = ({ - className, - ...props -}: React.HTMLAttributes) => ( -
-); -DialogHeader.displayName = 'DialogHeader'; - -const DialogFooter = ({ - className, - ...props -}: React.HTMLAttributes) => ( -
-); -DialogFooter.displayName = 'DialogFooter'; - -const DialogTitle = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className, ...props }, ref) => ( - -)); -DialogTitle.displayName = DialogPrimitive.Title.displayName; - -const DialogDescription = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className, ...props }, ref) => ( - -)); -DialogDescription.displayName = DialogPrimitive.Description.displayName; - -export { - Dialog, - DialogPortal, - DialogOverlay, - DialogClose, - DialogTrigger, - DialogContent, - DialogHeader, - DialogFooter, - DialogTitle, - DialogDescription, -}; diff --git a/apps/dashboard/src/components/ui/scroll-area.tsx b/apps/dashboard/src/components/ui/scroll-area.tsx deleted file mode 100644 index 2775243e..00000000 --- a/apps/dashboard/src/components/ui/scroll-area.tsx +++ /dev/null @@ -1,56 +0,0 @@ -'use client'; - -import { cn } from '@/utils/cn'; -import * as ScrollAreaPrimitive from '@radix-ui/react-scroll-area'; -import * as React from 'react'; - -const ScrollArea = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef & { - orientation?: 'vertical' | 'horizontal'; - } ->(({ className, children, orientation = 'vertical', ...props }, ref) => ( - - - {children} - - - - -)); -ScrollArea.displayName = ScrollAreaPrimitive.Root.displayName; - -const ScrollBar = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className, orientation = 'vertical', ...props }, ref) => ( - - - -)); -ScrollBar.displayName = ScrollAreaPrimitive.ScrollAreaScrollbar.displayName; - -export { ScrollArea, ScrollBar }; diff --git a/apps/dashboard/src/components/ui/table.tsx b/apps/dashboard/src/components/ui/table.tsx deleted file mode 100644 index 307c984e..00000000 --- a/apps/dashboard/src/components/ui/table.tsx +++ /dev/null @@ -1,155 +0,0 @@ -'use client'; - -import { cn } from '@/utils/cn'; -import * as React from 'react'; - -const Table = React.forwardRef< - HTMLTableElement, - React.HTMLAttributes & { - overflow?: boolean; - } ->(({ className, overflow = true, ...props }, ref) => ( -
-
- - - -)); -Table.displayName = 'Table'; - -const TableHeader = React.forwardRef< - HTMLTableSectionElement, - React.HTMLAttributes ->(({ className, ...props }, ref) => ( - -)); -TableHeader.displayName = 'TableHeader'; - -const TableBody = React.forwardRef< - HTMLTableSectionElement, - React.HTMLAttributes ->(({ className, ...props }, ref) => ( - -)); -TableBody.displayName = 'TableBody'; - -const TableFooter = React.forwardRef< - HTMLTableSectionElement, - React.HTMLAttributes ->(({ className, ...props }, ref) => ( - -)); -TableFooter.displayName = 'TableFooter'; - -const TableRow = React.forwardRef< - HTMLTableRowElement, - React.HTMLAttributes ->(({ className, ...props }, ref) => ( - -)); -TableRow.displayName = 'TableRow'; - -const TableHead = React.forwardRef< - HTMLTableCellElement, - React.ThHTMLAttributes ->(({ className, ...props }, ref) => ( -
-)); -TableHead.displayName = 'TableHead'; - -const TableCell = React.forwardRef< - HTMLTableCellElement, - React.TdHTMLAttributes ->(({ className, ...props }, ref) => ( - -)); -TableCell.displayName = 'TableCell'; - -const TableCaption = React.forwardRef< - HTMLTableCaptionElement, - React.HTMLAttributes ->(({ className, ...props }, ref) => ( -
-)); -TableCaption.displayName = 'TableCaption'; - -export function TableSkeleton({ - rows = 10, - cols = 2, -}: { - rows?: number; - cols?: number; -}) { - return ( - - - - {Array.from({ length: cols }).map((_, j) => ( - -
- - ))} - - - - {Array.from({ length: rows }).map((_, i) => ( - - {Array.from({ length: cols }).map((_, j) => ( - -
- - ))} - - ))} - -
- ); -} - -export { - Table, - TableHeader, - TableBody, - TableFooter, - TableHead, - TableRow, - TableCell, - TableCaption, -}; diff --git a/apps/dashboard/src/env.mjs b/apps/dashboard/src/env.mjs deleted file mode 100644 index b25d1ae3..00000000 --- a/apps/dashboard/src/env.mjs +++ /dev/null @@ -1,47 +0,0 @@ -import { createEnv } from '@t3-oss/env-nextjs'; -import { z } from 'zod'; - -export const env = createEnv({ - /** - * Specify your server-side environment variables schema here. This way you can ensure the app - * isn't built with invalid env vars. - */ - server: { - DATABASE_URL: z - .string() - .url() - .refine( - (str) => !str.includes('DATABASE_URL'), - 'You forgot to change the default URL', - ), - NODE_ENV: z - .enum(['development', 'test', 'production']) - .default('development'), - }, - - /** - * Specify your client-side environment variables schema here. This way you can ensure the app - * isn't built with invalid env vars. To expose them to the client, prefix them with - * `NEXT_PUBLIC_`. - */ - client: {}, - - /** - * You can't destruct `process.env` as a regular object in the Next.js edge runtimes (e.g. - * middlewares) or client-side so we need to destruct manually. - */ - runtimeEnv: { - DATABASE_URL: process.env.DATABASE_URL, - NODE_ENV: process.env.NODE_ENV, - }, - /** - * Run `build` or `dev` with `SKIP_ENV_VALIDATION` to skip env validation. This is especially - * useful for Docker builds. - */ - skipValidation: !!process.env.SKIP_ENV_VALIDATION, - /** - * Makes it so that empty strings are treated as undefined. - * `SOME_VAR: z.string()` and `SOME_VAR=''` will throw an error. - */ - emptyStringAsUndefined: true, -}); diff --git a/apps/dashboard/src/hocs/with-loading-widget.tsx b/apps/dashboard/src/hocs/with-loading-widget.tsx deleted file mode 100644 index aa2972fb..00000000 --- a/apps/dashboard/src/hocs/with-loading-widget.tsx +++ /dev/null @@ -1,37 +0,0 @@ -import { Widget, WidgetHead } from '@/components/widget'; -import { cn } from '@/utils/cn'; -import { Suspense } from 'react'; - -const withLoadingWidget = (Component: React.ComponentType

) => { - const WithLoadingWidget: React.ComponentType

= (props) => { - return ( - - - Loading... - -

- - } - > - - - ); - }; - - WithLoadingWidget.displayName = `WithLoadingWidget(${Component.displayName})`; - - return WithLoadingWidget; -}; - -export default withLoadingWidget; diff --git a/apps/dashboard/src/hocs/with-suspense.tsx b/apps/dashboard/src/hocs/with-suspense.tsx deleted file mode 100644 index 3fb92273..00000000 --- a/apps/dashboard/src/hocs/with-suspense.tsx +++ /dev/null @@ -1,22 +0,0 @@ -import { Suspense } from 'react'; - -const withSuspense = ( - Component: React.ComponentType

, - Fallback: React.ComponentType

, -) => { - const WithSuspense: React.ComponentType

= (props) => { - const fallback = ; - // return <>{fallback}; - return ( - - - - ); - }; - - WithSuspense.displayName = `WithSuspense(${Component.displayName})`; - - return WithSuspense; -}; - -export default withSuspense; diff --git a/apps/dashboard/src/hooks/useAppParams.ts b/apps/dashboard/src/hooks/useAppParams.ts deleted file mode 100644 index 83e3de54..00000000 --- a/apps/dashboard/src/hooks/useAppParams.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { useParams } from 'next/navigation'; - -type AppParams = { - organizationId: string; - projectId: string; -}; - -export function useAppParams() { - const params = useParams(); - return { - ...(params ?? {}), - organizationId: params?.organizationSlug, - projectId: params?.projectId, - } as T & AppParams; -} diff --git a/apps/dashboard/src/hooks/useAuth.tsx b/apps/dashboard/src/hooks/useAuth.tsx deleted file mode 100644 index 43504add..00000000 --- a/apps/dashboard/src/hooks/useAuth.tsx +++ /dev/null @@ -1,5 +0,0 @@ -import { api } from '@/trpc/client'; - -export function useAuth() { - return api.auth.session.useQuery(); -} diff --git a/apps/dashboard/src/hooks/useCursor.ts b/apps/dashboard/src/hooks/useCursor.ts deleted file mode 100644 index 12e890eb..00000000 --- a/apps/dashboard/src/hooks/useCursor.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { parseAsInteger, useQueryState } from 'nuqs'; -import { useTransition } from 'react'; - -import { useDebounceValue } from './useDebounceValue'; - -export function useCursor() { - const [loading, startTransition] = useTransition(); - const [cursor, setCursor] = useQueryState( - 'cursor', - parseAsInteger - .withOptions({ shallow: false, history: 'push', startTransition }) - .withDefault(0), - ); - return { - cursor, - setCursor, - loading, - }; -} - -export type UseDebouncedCursor = ReturnType; - -export function useDebouncedCursor() { - const [cursor, setCursor] = useQueryState( - 'cursor', - parseAsInteger.withDefault(0), - ); - const debouncedCursor = useDebounceValue(cursor, 200); - return { - value: cursor, - set: setCursor, - debounced: debouncedCursor, - }; -} diff --git a/apps/dashboard/src/hooks/useEventNames.ts b/apps/dashboard/src/hooks/useEventNames.ts deleted file mode 100644 index f98901c9..00000000 --- a/apps/dashboard/src/hooks/useEventNames.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { api } from '@/trpc/client'; - -export function useEventNames( - params: Parameters[0], -) { - const query = api.chart.events.useQuery(params, { - staleTime: 1000 * 60 * 10, - }); - return query.data ?? []; -} diff --git a/apps/dashboard/src/hooks/useEventProperties.ts b/apps/dashboard/src/hooks/useEventProperties.ts deleted file mode 100644 index 2b256db6..00000000 --- a/apps/dashboard/src/hooks/useEventProperties.ts +++ /dev/null @@ -1,15 +0,0 @@ -import type { RouterInputs } from '@/trpc/client'; -import { api } from '@/trpc/client'; -import type { UseQueryOptions } from '@tanstack/react-query'; - -export function useEventProperties( - params: RouterInputs['chart']['properties'], - options?: UseQueryOptions, -): string[] { - const query = api.chart.properties.useQuery(params, { - staleTime: 1000 * 60 * 10, - enabled: options?.enabled ?? true, - }); - - return query.data ?? []; -} diff --git a/apps/dashboard/src/hooks/useLogout.ts b/apps/dashboard/src/hooks/useLogout.ts deleted file mode 100644 index 6b7cda6b..00000000 --- a/apps/dashboard/src/hooks/useLogout.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { api } from '@/trpc/client'; -import { useRouter } from 'next/navigation'; - -export function useLogout() { - const router = useRouter(); - const signOut = api.auth.signOut.useMutation({ - onSuccess() { - setTimeout(() => { - router.push('/login'); - }, 0); - }, - }); - - return () => signOut.mutate(); -} diff --git a/apps/dashboard/src/hooks/useMappings.ts b/apps/dashboard/src/hooks/useMappings.ts deleted file mode 100644 index c32a1b4e..00000000 --- a/apps/dashboard/src/hooks/useMappings.ts +++ /dev/null @@ -1,13 +0,0 @@ -import mappings from '@/mappings.json'; - -export function useMappings() { - return (val: string | string[]): string => { - if (Array.isArray(val)) { - return val - .map((v) => mappings.find((item) => item.id === v)?.name ?? v) - .join(''); - } - - return mappings.find((item) => item.id === val)?.name ?? val; - }; -} diff --git a/apps/dashboard/src/hooks/useProfileProperties.ts b/apps/dashboard/src/hooks/useProfileProperties.ts deleted file mode 100644 index f61ef005..00000000 --- a/apps/dashboard/src/hooks/useProfileProperties.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { api } from '@/trpc/client'; - -export function useProfileProperties(projectId: string) { - const query = api.profile.properties.useQuery({ - projectId: projectId, - }); - - return query.data ?? []; -} diff --git a/apps/dashboard/src/hooks/useProfileValues.ts b/apps/dashboard/src/hooks/useProfileValues.ts deleted file mode 100644 index d850f6fa..00000000 --- a/apps/dashboard/src/hooks/useProfileValues.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { api } from '@/trpc/client'; - -export function useProfileValues(projectId: string, property: string) { - const query = api.profile.values.useQuery( - { - projectId: projectId, - property, - }, - { - staleTime: 1000 * 60 * 10, - }, - ); - - return query.data?.values ?? []; -} diff --git a/apps/dashboard/src/hooks/usePropertyValues.ts b/apps/dashboard/src/hooks/usePropertyValues.ts deleted file mode 100644 index 227b8e12..00000000 --- a/apps/dashboard/src/hooks/usePropertyValues.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { api } from '@/trpc/client'; - -export function usePropertyValues( - params: Parameters[0], -) { - const query = api.chart.values.useQuery(params, { - staleTime: 1000 * 60 * 10, - }); - - return query.data?.values ?? []; -} diff --git a/apps/dashboard/src/instrumentation.ts b/apps/dashboard/src/instrumentation.ts deleted file mode 100644 index c8c76462..00000000 --- a/apps/dashboard/src/instrumentation.ts +++ /dev/null @@ -1,10 +0,0 @@ -export async function register() { - if (process.env.NEXT_RUNTIME === 'nodejs' && process.env.HYPERDX_API_KEY) { - const { initSDK } = await import('@hyperdx/node-opentelemetry'); - initSDK({ - consoleCapture: true, - apiKey: process.env.HYPERDX_API_KEY, - service: 'dashboard', - }); - } -} diff --git a/apps/dashboard/src/mappings.json b/apps/dashboard/src/mappings.json deleted file mode 100644 index f2d7122d..00000000 --- a/apps/dashboard/src/mappings.json +++ /dev/null @@ -1,6 +0,0 @@ -[ - { - "id": "123", - "name": "123" - } -] diff --git a/apps/dashboard/src/middleware.ts b/apps/dashboard/src/middleware.ts deleted file mode 100644 index d7154a5b..00000000 --- a/apps/dashboard/src/middleware.ts +++ /dev/null @@ -1,100 +0,0 @@ -import { COOKIE_MAX_AGE, COOKIE_OPTIONS } from '@openpanel/auth/constants'; -import { type NextRequest, NextResponse } from 'next/server'; - -function createRouteMatcher(patterns: string[]) { - // Convert route patterns to regex patterns - const regexPatterns = patterns.map((pattern) => { - // Replace route parameters (:id) with regex capture groups - const regexPattern = pattern - .replace(/\//g, '\\/') // Escape forward slashes - .replace(/:\w+/g, '([^/]+)') // Convert :param to capture groups - .replace(/\(\.\*\)\?/g, '(?:.*)?'); // Handle optional wildcards - - return new RegExp(`^${regexPattern}$`); - }); - - // Return a matcher function - return (req: { url: string }) => { - const pathname = new URL(req.url).pathname; - return regexPatterns.some((regex) => regex.test(pathname)); - }; -} - -// This example protects all routes including api/trpc routes -// Please edit this to allow other routes to be public as needed. -const isPublicRoute = createRouteMatcher([ - '/share/overview/:id', - '/login(.*)?', - '/reset-password(.*)?', - '/sso-callback(.*)?', - '/onboarding', - '/maintenance', - '/api/headers', -]); - -export default (request: NextRequest) => { - // Check for maintenance mode - if ( - process.env.MAINTENANCE === 'true' && - !request.nextUrl.pathname.startsWith('/maintenance') - ) { - return NextResponse.redirect(new URL('/maintenance', request.url)); - } - - if (request.method === 'GET') { - const response = NextResponse.next(); - const token = request.cookies.get('session')?.value ?? null; - - if (process.env.DEMO_USER_ID) { - return response; - } - - if (!isPublicRoute(request) && token === null) { - return NextResponse.redirect(new URL('/login', request.url)); - } - - if (token !== null) { - // Only extend cookie expiration on GET requests since we can be sure - // a new session wasn't set when handling the request. - response.cookies.set('session', token, { - maxAge: COOKIE_MAX_AGE, - ...COOKIE_OPTIONS, - }); - } - return response; - } - - const originHeader = request.headers.get('Origin'); - // NOTE: You may need to use `X-Forwarded-Host` instead - const hostHeader = request.headers.get('Host'); - if (originHeader === null || hostHeader === null) { - return new NextResponse(null, { - status: 403, - }); - } - let origin: URL; - try { - origin = new URL(originHeader); - } catch { - return new NextResponse(null, { - status: 403, - }); - } - if (origin.host !== hostHeader) { - return new NextResponse(null, { - status: 403, - }); - } - - return NextResponse.next(); -}; - -export const config = { - matcher: [ - '/((?!.+\\.[\\w]+$|_next).*)', - '/', - '/(api)(.*)', - '/(api|trpc)(.*)', - '/api/trpc(.*)', - ], -}; diff --git a/apps/dashboard/src/modals/SaveReport.tsx b/apps/dashboard/src/modals/SaveReport.tsx deleted file mode 100644 index e5b861e3..00000000 --- a/apps/dashboard/src/modals/SaveReport.tsx +++ /dev/null @@ -1,150 +0,0 @@ -import { ButtonContainer } from '@/components/button-container'; -import { InputWithLabel } from '@/components/forms/input-with-label'; -import { Button } from '@/components/ui/button'; -import { Combobox } from '@/components/ui/combobox'; -import { Label } from '@/components/ui/label'; -import { useAppParams } from '@/hooks/useAppParams'; -import { api, handleError } from '@/trpc/client'; -import { zodResolver } from '@hookform/resolvers/zod'; -import { useRouter, useSearchParams } from 'next/navigation'; -import { Controller, useForm } from 'react-hook-form'; -import { toast } from 'sonner'; -import { z } from 'zod'; - -import type { IChartProps } from '@openpanel/validation'; - -import { popModal } from '.'; -import { ModalContent, ModalHeader } from './Modal/Container'; - -type SaveReportProps = { - report: IChartProps; - disableRedirect?: boolean; -}; - -const validator = z.object({ - name: z.string().min(1, 'Required'), - dashboardId: z.string().min(1, 'Required'), -}); - -type IForm = z.infer; - -export default function SaveReport({ - report, - disableRedirect, -}: SaveReportProps) { - const router = useRouter(); - const { organizationId, projectId } = useAppParams(); - const searchParams = useSearchParams(); - const dashboardId = searchParams?.get('dashboardId') ?? undefined; - - const save = api.report.create.useMutation({ - onError: handleError, - onSuccess(res) { - const goToReport = () => { - router.push( - `/${organizationId}/${projectId}/reports/${ - res.id - }?${searchParams?.toString()}`, - ); - }; - - toast('Report created', { - description: `${res.name}`, - action: { - label: 'View report', - onClick: () => goToReport(), - }, - }); - - if (!disableRedirect) { - goToReport(); - } - - popModal(); - }, - }); - - const { register, handleSubmit, formState, control, setValue } = - useForm({ - resolver: zodResolver(validator), - defaultValues: { - name: report.name, - dashboardId, - }, - }); - - const dashboardMutation = api.dashboard.create.useMutation({ - onError: handleError, - onSuccess(res) { - setValue('dashboardId', res.id); - dashboardQuery.refetch(); - toast('Success', { - description: 'Dashboard created.', - }); - }, - }); - - const dashboardQuery = api.dashboard.list.useQuery({ - projectId, - }); - const dashboards = (dashboardQuery.data ?? []).map((item) => ({ - value: item.id, - label: item.name, - })); - - return ( - - -

{ - save.mutate({ - report: { - ...report, - name, - }, - ...values, - }); - })} - > - - { - return ( -
- - { - dashboardMutation.mutate({ - projectId, - name: value, - }); - }} - /> -
- ); - }} - /> - - - - - - - ); -} diff --git a/apps/dashboard/src/modals/Testimonial.tsx b/apps/dashboard/src/modals/Testimonial.tsx deleted file mode 100644 index 950a36a5..00000000 --- a/apps/dashboard/src/modals/Testimonial.tsx +++ /dev/null @@ -1,78 +0,0 @@ -import { Button } from '@/components/ui/button'; -import { Textarea } from '@/components/ui/textarea'; -import { useAppParams } from '@/hooks/useAppParams'; -import { api } from '@/trpc/client'; -import { zodResolver } from '@hookform/resolvers/zod'; -import { useForm } from 'react-hook-form'; -import { toast } from 'sonner'; -import { z } from 'zod'; - -import { useOpenPanel } from '@openpanel/nextjs'; - -import { popModal } from '.'; -import { ModalContent } from './Modal/Container'; - -const validator = z.object({ - body: z.string().min(3), -}); - -type IForm = z.infer; - -const Testimonial = () => { - const op = useOpenPanel(); - const mutation = api.ticket.create.useMutation(); - const params = useAppParams(); - const form = useForm({ - resolver: zodResolver(validator), - }); - return ( - -
-

Review time 🫶

-

- Thank you so much for using Openpanel — it truly means a great deal to - me! If you're enjoying your experience, I'd be thrilled if - you could leave a quick review. 😇 -

-

- If you have any feedback or suggestions, I'd love to hear them as - well! 🚀 -

-
-
{ - try { - await mutation.mutateAsync({ - subject: 'New testimonial', - body, - meta: { - ...params, - }, - }); - toast.success('Thanks for your feedback 🚀'); - op.track('testimonials_sent'); - popModal(); - } catch (e) { - toast.error('Something went wrong. Please try again later.'); - } - })} - > -