From 8c72bf3d5a3f2c9213f0fb20c26b9d312e97a489 Mon Sep 17 00:00:00 2001 From: Renato Date: Mon, 3 Aug 2026 23:30:42 +0200 Subject: [PATCH 01/42] Migrate to React Router v8, pnpm workspace, Tailwind + shadcn Collapse the frontend/backend split into one React Router app and replace lerna with a pnpm workspace. Structure: - apps/web React Router 8 framework mode (dashboard + /collect + tracker) - apps/docs Nextra 1 -> 4 (Next 12 -> 16, app router + content dir) - packages/tracker tracker script, esbuild -> apps/web/public/tracker.js Web app: - CRA/react-scripts -> Vite; Chakra v1 -> Tailwind v4 + shadcn (Base UI) - React 17 -> 19, Prisma 3 -> 7 (driver adapter + prisma.config.ts) - SWR/axios hooks -> loaders; the 8 dashboard requests are now one loader, with range/tz in the URL so the view is shareable - Vercel serverless handlers -> loaders/actions. Only /collect and /collect/:id stay HTTP (tracker calls them cross-origin); CORS is scoped to those two routes instead of every response - JWT in localStorage -> signed httpOnly cookie sessions; the client-side route guard becomes a layout loader redirect - Jest/node-mocks-http -> Vitest (38 tests); CI Node 12/14/16 -> 22 Fixes found while porting: - timeseries built SQL via $queryRawUnsafe with wid/tz/dates interpolated; now parameter-bound, with unit/tz allow-listed (bad tz -> 400) - POST /setup never checked whether a user already existed, so anyone could create an account on a live instance; both loader and action check now - DELETE /websites/:id never verified ownership - /collect/:id updated an event by id alone; now scoped to its website Co-Authored-By: Claude Opus 5 (1M context) --- .dockerignore | 10 + .github/workflows/backend.yml | 54 - .github/workflows/ci.yml | 69 + .github/workflows/frontend.yml | 30 - .gitignore | 26 +- Dockerfile | 36 + README.md | 49 +- apps/docs/.gitignore | 4 + apps/docs/app/[[...mdxPath]]/page.jsx | 25 + apps/docs/app/layout.jsx | 56 + apps/docs/content/_meta.js | 4 + apps/docs/content/get-started.mdx | 116 + apps/docs/content/index.mdx | 34 + apps/docs/mdx-components.js | 7 + apps/docs/next.config.mjs | 9 + {packages => apps}/docs/package.json | 20 +- {packages => apps}/docs/public/aurora.svg | 0 {packages => apps}/docs/public/favicon.ico | Bin apps/web/.env.example | 5 + apps/web/.gitignore | 13 + apps/web/README.md | 92 + apps/web/app/app.css | 130 + .../app/components/analytics-dashboard.tsx | 128 + apps/web/app/components/breakdown-table.tsx | 42 + apps/web/app/components/footer.tsx | 15 + apps/web/app/components/logo.tsx | 18 + apps/web/app/components/navbar.tsx | 56 + apps/web/app/components/page-header.tsx | 31 + apps/web/app/components/shell.tsx | 21 + apps/web/app/components/theme-toggle.tsx | 37 + apps/web/app/components/timeseries-chart.tsx | 82 + apps/web/app/components/ui/alert-dialog.tsx | 185 + apps/web/app/components/ui/alert.tsx | 76 + apps/web/app/components/ui/avatar.tsx | 109 + apps/web/app/components/ui/badge.tsx | 52 + apps/web/app/components/ui/button.tsx | 58 + apps/web/app/components/ui/card.tsx | 103 + apps/web/app/components/ui/dropdown-menu.tsx | 270 + apps/web/app/components/ui/field.tsx | 236 + apps/web/app/components/ui/input.tsx | 20 + apps/web/app/components/ui/label.tsx | 18 + apps/web/app/components/ui/select.tsx | 200 + apps/web/app/components/ui/separator.tsx | 23 + apps/web/app/components/ui/skeleton.tsx | 13 + apps/web/app/components/ui/switch.tsx | 32 + apps/web/app/components/ui/table.tsx | 116 + apps/web/app/components/ui/tooltip.tsx | 66 + apps/web/app/components/website-form.tsx | 106 + apps/web/app/layouts/app.tsx | 23 + apps/web/app/lib/analytics.server.ts | 73 + apps/web/app/lib/analytics.test.ts | 58 + apps/web/app/lib/cors.server.ts | 23 + apps/web/app/lib/db.server.ts | 27 + .../web/app/lib/hash.server.ts | 4 +- apps/web/app/lib/metrics.server.ts | 140 + apps/web/app/lib/metrics.test.ts | 146 + apps/web/app/lib/queries.server.ts | 212 + apps/web/app/lib/session.server.ts | 92 + apps/web/app/lib/types.ts | 26 + apps/web/app/lib/ua.server.ts | 52 + apps/web/app/lib/ua.test.ts | 44 + apps/web/app/lib/utils.ts | 6 + apps/web/app/lib/validation.test.ts | 121 + apps/web/app/lib/validation.ts | 68 + apps/web/app/lib/website-access.server.ts | 45 + apps/web/app/root.tsx | 85 + apps/web/app/routes.ts | 30 + apps/web/app/routes/account.tsx | 141 + apps/web/app/routes/analytics.public.tsx | 33 + apps/web/app/routes/analytics.tsx | 39 + apps/web/app/routes/collect.$id.ts | 59 + apps/web/app/routes/collect.ts | 91 + apps/web/app/routes/logout.tsx | 12 + apps/web/app/routes/not-found.tsx | 18 + apps/web/app/routes/setup.tsx | 136 + apps/web/app/routes/signin.tsx | 122 + apps/web/app/routes/websites.edit.tsx | 134 + apps/web/app/routes/websites.new.tsx | 57 + apps/web/app/routes/websites.tsx | 103 + apps/web/components.json | 25 + apps/web/package.json | 72 + apps/web/prisma.config.ts | 22 + .../migrations/20220426100738_/migration.sql | 0 .../migrations/20220428160417_/migration.sql | 0 .../prisma/migrations/migration_lock.toml | 0 .../backend => apps/web}/prisma/schema.prisma | 8 +- apps/web/prisma/seed.ts | 35 + .../web}/public/aurora_mini_blue.svg | 0 apps/web/public/favicon.ico | Bin 0 -> 15086 bytes .../frontend => apps/web}/public/robots.txt | 0 apps/web/react-router.config.ts | 7 + apps/web/test/setup.ts | 3 + apps/web/tsconfig.json | 26 + apps/web/vite.config.ts | 10 + apps/web/vitest.config.ts | 22 + lerna.json | 6 - package-lock.json | 6324 ------- package.json | 27 +- packages/backend/.env.example | 2 - packages/backend/.gitignore | 35 - packages/backend/__tests__/me.test.js | 129 - packages/backend/__tests__/setup.test.js | 177 - packages/backend/__tests__/signin.test.js | 98 - packages/backend/__tests__/website.test.js | 332 - packages/backend/__tests__/websites.test.js | 166 - packages/backend/api/_files/tracker.js | 1 - packages/backend/api/collect/[id].js | 16 - packages/backend/api/collect/index.js | 15 - packages/backend/api/me.js | 17 - packages/backend/api/setup.js | 17 - packages/backend/api/signin.js | 15 - packages/backend/api/tracker.js | 17 - packages/backend/api/websites/[id]/index.js | 19 - .../api/websites/[id]/metrics/metadata.js | 15 - .../api/websites/[id]/metrics/pages.js | 15 - .../api/websites/[id]/metrics/statistics.js | 15 - .../api/websites/[id]/metrics/timeseries.js | 15 - packages/backend/api/websites/index.js | 17 - packages/backend/babel.config.js | 15 - .../backend/lib/__tests__/database.test.js | 151 - packages/backend/lib/client.js | 5 - .../lib/controllers/auth-controller.js | 39 - .../lib/controllers/collect-controller.js | 121 - .../backend/lib/controllers/controller.js | 64 - .../backend/lib/controllers/me-controller.js | 35 - .../lib/controllers/metadata-controller.js | 67 - .../lib/controllers/pages-controller.js | 60 - .../lib/controllers/setup-controller.js | 34 - .../lib/controllers/statistics-controller.js | 43 - .../lib/controllers/timeseries-controller.js | 117 - .../lib/controllers/website-controller.js | 81 - packages/backend/lib/database.js | 265 - packages/backend/lib/errors.js | 6 - packages/backend/lib/jest.js | 18 - .../backend/lib/middleware/authentication.js | 39 - .../backend/lib/middleware/with-preflight.js | 11 - packages/backend/lib/utils/generate.js | 68 - packages/backend/lib/utils/tag.js | 5 - packages/backend/lib/utils/ua.js | 52 - packages/backend/package-lock.json | 6147 ------- packages/backend/package.json | 44 - packages/backend/prisma/seed.js | 24 - packages/backend/tracker.config.js | 17 - packages/backend/vercel.json | 30 - packages/docs/.eslintrc.json | 3 - packages/docs/.gitignore | 32 - packages/docs/next.config.js | 7 - packages/docs/package-lock.json | 5369 ------ packages/docs/pages/_app.js | 7 - packages/docs/pages/get-started.mdx | 132 - packages/docs/pages/index.mdx | 48 - packages/docs/pages/meta.json | 4 - packages/docs/theme.config.js | 43 - packages/frontend/.env.example | 1 - packages/frontend/.gitignore | 24 - packages/frontend/cypress.json | 6 - .../frontend/cypress/fixtures/example.json | 5 - .../integration/authentication.spec.js | 19 - .../cypress/integration/websites.spec.js | 65 - packages/frontend/cypress/plugins/index.js | 22 - packages/frontend/cypress/support/commands.js | 25 - packages/frontend/cypress/support/index.js | 20 - packages/frontend/package-lock.json | 13938 ---------------- packages/frontend/package.json | 70 - packages/frontend/public/favicon.ico | Bin 15086 -> 0 bytes packages/frontend/public/index.html | 17 - packages/frontend/public/logo192.png | Bin 5347 -> 0 bytes packages/frontend/public/logo512.png | Bin 9664 -> 0 bytes packages/frontend/public/manifest.json | 25 - packages/frontend/src/App.tsx | 71 - .../frontend/src/components/Footer/Footer.jsx | 18 - .../frontend/src/components/Footer/index.js | 1 - .../frontend/src/components/Loader/Loader.tsx | 16 - .../frontend/src/components/Loader/index.ts | 1 - .../frontend/src/components/Logo/Logo.jsx | 12 - .../frontend/src/components/Logo/index.js | 1 - .../src/components/MainLayout/MainLayout.tsx | 28 - .../src/components/MainLayout/index.ts | 1 - .../frontend/src/components/Navbar/Navbar.tsx | 46 - .../src/components/Navbar/NavbarLink.tsx | 14 - .../frontend/src/components/Navbar/index.ts | 1 - .../frontend/src/components/Panel/Panel.tsx | 35 - .../src/components/Panel/PanelBody.tsx | 12 - .../src/components/Panel/PanelTitle.tsx | 15 - .../frontend/src/components/Panel/index.ts | 3 - .../frontend/src/components/Stat/Stat.tsx | 23 - .../frontend/src/components/Stat/index.ts | 1 - .../components/ToggleButton/ToggleButton.jsx | 16 - .../src/components/ToggleButton/index.js | 1 - .../components/WebsitesForm/WebsitesForm.tsx | 85 - .../src/components/WebsitesForm/index.ts | 1 - .../src/components/Wrapper/Wrapper.tsx | 16 - .../src/components/Wrapper/WrapperActions.tsx | 12 - .../src/components/Wrapper/WrapperContent.tsx | 12 - .../src/components/Wrapper/WrapperHeader.tsx | 22 - .../src/components/Wrapper/WrapperTitle.tsx | 11 - .../frontend/src/components/Wrapper/index.ts | 6 - packages/frontend/src/index.js | 21 - packages/frontend/src/lib/client.js | 5 - .../frontend/src/lib/context/auth-context.js | 44 - .../frontend/src/lib/hooks/use-account.ts | 15 - .../src/lib/hooks/use-aurora-links.js | 17 - packages/frontend/src/lib/hooks/use-form.js | 44 - .../frontend/src/lib/hooks/use-metadata.js | 20 - packages/frontend/src/lib/hooks/use-pages.js | 18 - .../frontend/src/lib/hooks/use-statistics.js | 18 - .../frontend/src/lib/hooks/use-timeseries.js | 18 - .../frontend/src/lib/hooks/use-website.js | 14 - .../frontend/src/lib/hooks/use-websites.ts | 15 - .../src/lib/reducers/filters-reducer.js | 36 - packages/frontend/src/lib/theme.js | 10 - .../frontend/src/pages/Account/Account.tsx | 21 - .../src/pages/Account/AccountForm.tsx | 100 - packages/frontend/src/pages/Account/index.ts | 1 - .../src/pages/Analytics/Analytics.tsx | 35 - .../src/pages/Analytics/AnalyticsDashboard.js | 58 - .../pages/Analytics/Charts/BrowserTable.tsx | 72 - .../pages/Analytics/Charts/CountryTable.js | 69 - .../src/pages/Analytics/Charts/DeviceTable.js | 69 - .../src/pages/Analytics/Charts/OsTable.js | 69 - .../src/pages/Analytics/Charts/PageTable.js | 69 - .../pages/Analytics/Charts/ReferrerTable.js | 86 - .../src/pages/Analytics/Charts/Stats.js | 29 - .../pages/Analytics/Charts/TimeseriesChart.js | 131 - .../frontend/src/pages/Analytics/index.js | 1 - .../src/pages/EditWebsite/EditWebsite.js | 117 - .../frontend/src/pages/EditWebsite/index.js | 1 - .../src/pages/NewWebsite/NewWebsite.tsx | 47 - .../frontend/src/pages/NewWebsite/index.ts | 1 - packages/frontend/src/pages/NotFound.tsx | 18 - packages/frontend/src/pages/Setup/Setup.tsx | 33 - .../frontend/src/pages/Setup/SetupForm.tsx | 85 - packages/frontend/src/pages/Setup/index.ts | 1 - packages/frontend/src/pages/SignIn/SignIn.tsx | 27 - .../frontend/src/pages/SignIn/SignInForm.tsx | 58 - packages/frontend/src/pages/SignIn/index.ts | 1 - .../frontend/src/pages/Websites/Websites.tsx | 35 - .../src/pages/Websites/WebsitesList.tsx | 77 - packages/frontend/src/pages/Websites/index.ts | 1 - packages/frontend/src/setupTests.js | 1 - packages/frontend/src/types.ts | 32 - packages/frontend/tsconfig.json | 20 - packages/tracker/package.json | 14 + .../lib/tracker => tracker/src}/aurora.js | 0 pnpm-lock.yaml | 10324 ++++++++++++ pnpm-workspace.yaml | 10 + 246 files changed, 15600 insertions(+), 37171 deletions(-) create mode 100644 .dockerignore delete mode 100644 .github/workflows/backend.yml create mode 100644 .github/workflows/ci.yml delete mode 100644 .github/workflows/frontend.yml create mode 100644 Dockerfile create mode 100644 apps/docs/.gitignore create mode 100644 apps/docs/app/[[...mdxPath]]/page.jsx create mode 100644 apps/docs/app/layout.jsx create mode 100644 apps/docs/content/_meta.js create mode 100644 apps/docs/content/get-started.mdx create mode 100644 apps/docs/content/index.mdx create mode 100644 apps/docs/mdx-components.js create mode 100644 apps/docs/next.config.mjs rename {packages => apps}/docs/package.json (53%) rename {packages => apps}/docs/public/aurora.svg (100%) rename {packages => apps}/docs/public/favicon.ico (100%) create mode 100644 apps/web/.env.example create mode 100644 apps/web/.gitignore create mode 100644 apps/web/README.md create mode 100644 apps/web/app/app.css create mode 100644 apps/web/app/components/analytics-dashboard.tsx create mode 100644 apps/web/app/components/breakdown-table.tsx create mode 100644 apps/web/app/components/footer.tsx create mode 100644 apps/web/app/components/logo.tsx create mode 100644 apps/web/app/components/navbar.tsx create mode 100644 apps/web/app/components/page-header.tsx create mode 100644 apps/web/app/components/shell.tsx create mode 100644 apps/web/app/components/theme-toggle.tsx create mode 100644 apps/web/app/components/timeseries-chart.tsx create mode 100644 apps/web/app/components/ui/alert-dialog.tsx create mode 100644 apps/web/app/components/ui/alert.tsx create mode 100644 apps/web/app/components/ui/avatar.tsx create mode 100644 apps/web/app/components/ui/badge.tsx create mode 100644 apps/web/app/components/ui/button.tsx create mode 100644 apps/web/app/components/ui/card.tsx create mode 100644 apps/web/app/components/ui/dropdown-menu.tsx create mode 100644 apps/web/app/components/ui/field.tsx create mode 100644 apps/web/app/components/ui/input.tsx create mode 100644 apps/web/app/components/ui/label.tsx create mode 100644 apps/web/app/components/ui/select.tsx create mode 100644 apps/web/app/components/ui/separator.tsx create mode 100644 apps/web/app/components/ui/skeleton.tsx create mode 100644 apps/web/app/components/ui/switch.tsx create mode 100644 apps/web/app/components/ui/table.tsx create mode 100644 apps/web/app/components/ui/tooltip.tsx create mode 100644 apps/web/app/components/website-form.tsx create mode 100644 apps/web/app/layouts/app.tsx create mode 100644 apps/web/app/lib/analytics.server.ts create mode 100644 apps/web/app/lib/analytics.test.ts create mode 100644 apps/web/app/lib/cors.server.ts create mode 100644 apps/web/app/lib/db.server.ts rename packages/backend/lib/utils/hash.js => apps/web/app/lib/hash.server.ts (57%) create mode 100644 apps/web/app/lib/metrics.server.ts create mode 100644 apps/web/app/lib/metrics.test.ts create mode 100644 apps/web/app/lib/queries.server.ts create mode 100644 apps/web/app/lib/session.server.ts create mode 100644 apps/web/app/lib/types.ts create mode 100644 apps/web/app/lib/ua.server.ts create mode 100644 apps/web/app/lib/ua.test.ts create mode 100644 apps/web/app/lib/utils.ts create mode 100644 apps/web/app/lib/validation.test.ts create mode 100644 apps/web/app/lib/validation.ts create mode 100644 apps/web/app/lib/website-access.server.ts create mode 100644 apps/web/app/root.tsx create mode 100644 apps/web/app/routes.ts create mode 100644 apps/web/app/routes/account.tsx create mode 100644 apps/web/app/routes/analytics.public.tsx create mode 100644 apps/web/app/routes/analytics.tsx create mode 100644 apps/web/app/routes/collect.$id.ts create mode 100644 apps/web/app/routes/collect.ts create mode 100644 apps/web/app/routes/logout.tsx create mode 100644 apps/web/app/routes/not-found.tsx create mode 100644 apps/web/app/routes/setup.tsx create mode 100644 apps/web/app/routes/signin.tsx create mode 100644 apps/web/app/routes/websites.edit.tsx create mode 100644 apps/web/app/routes/websites.new.tsx create mode 100644 apps/web/app/routes/websites.tsx create mode 100644 apps/web/components.json create mode 100644 apps/web/package.json create mode 100644 apps/web/prisma.config.ts rename {packages/backend => apps/web}/prisma/migrations/20220426100738_/migration.sql (100%) rename {packages/backend => apps/web}/prisma/migrations/20220428160417_/migration.sql (100%) rename {packages/backend => apps/web}/prisma/migrations/migration_lock.toml (100%) rename {packages/backend => apps/web}/prisma/schema.prisma (87%) create mode 100644 apps/web/prisma/seed.ts rename {packages/frontend => apps/web}/public/aurora_mini_blue.svg (100%) create mode 100644 apps/web/public/favicon.ico rename {packages/frontend => apps/web}/public/robots.txt (100%) create mode 100644 apps/web/react-router.config.ts create mode 100644 apps/web/test/setup.ts create mode 100644 apps/web/tsconfig.json create mode 100644 apps/web/vite.config.ts create mode 100644 apps/web/vitest.config.ts delete mode 100644 lerna.json delete mode 100644 package-lock.json delete mode 100644 packages/backend/.env.example delete mode 100644 packages/backend/.gitignore delete mode 100644 packages/backend/__tests__/me.test.js delete mode 100644 packages/backend/__tests__/setup.test.js delete mode 100644 packages/backend/__tests__/signin.test.js delete mode 100644 packages/backend/__tests__/website.test.js delete mode 100644 packages/backend/__tests__/websites.test.js delete mode 100644 packages/backend/api/_files/tracker.js delete mode 100644 packages/backend/api/collect/[id].js delete mode 100644 packages/backend/api/collect/index.js delete mode 100644 packages/backend/api/me.js delete mode 100644 packages/backend/api/setup.js delete mode 100644 packages/backend/api/signin.js delete mode 100644 packages/backend/api/tracker.js delete mode 100644 packages/backend/api/websites/[id]/index.js delete mode 100644 packages/backend/api/websites/[id]/metrics/metadata.js delete mode 100644 packages/backend/api/websites/[id]/metrics/pages.js delete mode 100644 packages/backend/api/websites/[id]/metrics/statistics.js delete mode 100644 packages/backend/api/websites/[id]/metrics/timeseries.js delete mode 100644 packages/backend/api/websites/index.js delete mode 100644 packages/backend/babel.config.js delete mode 100644 packages/backend/lib/__tests__/database.test.js delete mode 100644 packages/backend/lib/client.js delete mode 100644 packages/backend/lib/controllers/auth-controller.js delete mode 100644 packages/backend/lib/controllers/collect-controller.js delete mode 100644 packages/backend/lib/controllers/controller.js delete mode 100644 packages/backend/lib/controllers/me-controller.js delete mode 100644 packages/backend/lib/controllers/metadata-controller.js delete mode 100644 packages/backend/lib/controllers/pages-controller.js delete mode 100644 packages/backend/lib/controllers/setup-controller.js delete mode 100644 packages/backend/lib/controllers/statistics-controller.js delete mode 100644 packages/backend/lib/controllers/timeseries-controller.js delete mode 100644 packages/backend/lib/controllers/website-controller.js delete mode 100644 packages/backend/lib/database.js delete mode 100644 packages/backend/lib/errors.js delete mode 100644 packages/backend/lib/jest.js delete mode 100644 packages/backend/lib/middleware/authentication.js delete mode 100644 packages/backend/lib/middleware/with-preflight.js delete mode 100644 packages/backend/lib/utils/generate.js delete mode 100644 packages/backend/lib/utils/tag.js delete mode 100644 packages/backend/lib/utils/ua.js delete mode 100644 packages/backend/package-lock.json delete mode 100644 packages/backend/package.json delete mode 100644 packages/backend/prisma/seed.js delete mode 100644 packages/backend/tracker.config.js delete mode 100644 packages/backend/vercel.json delete mode 100644 packages/docs/.eslintrc.json delete mode 100644 packages/docs/.gitignore delete mode 100644 packages/docs/next.config.js delete mode 100644 packages/docs/package-lock.json delete mode 100644 packages/docs/pages/_app.js delete mode 100644 packages/docs/pages/get-started.mdx delete mode 100644 packages/docs/pages/index.mdx delete mode 100644 packages/docs/pages/meta.json delete mode 100644 packages/docs/theme.config.js delete mode 100644 packages/frontend/.env.example delete mode 100644 packages/frontend/.gitignore delete mode 100644 packages/frontend/cypress.json delete mode 100644 packages/frontend/cypress/fixtures/example.json delete mode 100644 packages/frontend/cypress/integration/authentication.spec.js delete mode 100644 packages/frontend/cypress/integration/websites.spec.js delete mode 100644 packages/frontend/cypress/plugins/index.js delete mode 100644 packages/frontend/cypress/support/commands.js delete mode 100644 packages/frontend/cypress/support/index.js delete mode 100644 packages/frontend/package-lock.json delete mode 100644 packages/frontend/package.json delete mode 100644 packages/frontend/public/favicon.ico delete mode 100644 packages/frontend/public/index.html delete mode 100644 packages/frontend/public/logo192.png delete mode 100644 packages/frontend/public/logo512.png delete mode 100644 packages/frontend/public/manifest.json delete mode 100644 packages/frontend/src/App.tsx delete mode 100644 packages/frontend/src/components/Footer/Footer.jsx delete mode 100644 packages/frontend/src/components/Footer/index.js delete mode 100644 packages/frontend/src/components/Loader/Loader.tsx delete mode 100644 packages/frontend/src/components/Loader/index.ts delete mode 100644 packages/frontend/src/components/Logo/Logo.jsx delete mode 100644 packages/frontend/src/components/Logo/index.js delete mode 100644 packages/frontend/src/components/MainLayout/MainLayout.tsx delete mode 100644 packages/frontend/src/components/MainLayout/index.ts delete mode 100644 packages/frontend/src/components/Navbar/Navbar.tsx delete mode 100644 packages/frontend/src/components/Navbar/NavbarLink.tsx delete mode 100644 packages/frontend/src/components/Navbar/index.ts delete mode 100644 packages/frontend/src/components/Panel/Panel.tsx delete mode 100644 packages/frontend/src/components/Panel/PanelBody.tsx delete mode 100644 packages/frontend/src/components/Panel/PanelTitle.tsx delete mode 100644 packages/frontend/src/components/Panel/index.ts delete mode 100644 packages/frontend/src/components/Stat/Stat.tsx delete mode 100644 packages/frontend/src/components/Stat/index.ts delete mode 100644 packages/frontend/src/components/ToggleButton/ToggleButton.jsx delete mode 100644 packages/frontend/src/components/ToggleButton/index.js delete mode 100644 packages/frontend/src/components/WebsitesForm/WebsitesForm.tsx delete mode 100644 packages/frontend/src/components/WebsitesForm/index.ts delete mode 100644 packages/frontend/src/components/Wrapper/Wrapper.tsx delete mode 100644 packages/frontend/src/components/Wrapper/WrapperActions.tsx delete mode 100644 packages/frontend/src/components/Wrapper/WrapperContent.tsx delete mode 100644 packages/frontend/src/components/Wrapper/WrapperHeader.tsx delete mode 100644 packages/frontend/src/components/Wrapper/WrapperTitle.tsx delete mode 100644 packages/frontend/src/components/Wrapper/index.ts delete mode 100644 packages/frontend/src/index.js delete mode 100644 packages/frontend/src/lib/client.js delete mode 100644 packages/frontend/src/lib/context/auth-context.js delete mode 100644 packages/frontend/src/lib/hooks/use-account.ts delete mode 100644 packages/frontend/src/lib/hooks/use-aurora-links.js delete mode 100644 packages/frontend/src/lib/hooks/use-form.js delete mode 100644 packages/frontend/src/lib/hooks/use-metadata.js delete mode 100644 packages/frontend/src/lib/hooks/use-pages.js delete mode 100644 packages/frontend/src/lib/hooks/use-statistics.js delete mode 100644 packages/frontend/src/lib/hooks/use-timeseries.js delete mode 100644 packages/frontend/src/lib/hooks/use-website.js delete mode 100644 packages/frontend/src/lib/hooks/use-websites.ts delete mode 100644 packages/frontend/src/lib/reducers/filters-reducer.js delete mode 100644 packages/frontend/src/lib/theme.js delete mode 100644 packages/frontend/src/pages/Account/Account.tsx delete mode 100644 packages/frontend/src/pages/Account/AccountForm.tsx delete mode 100644 packages/frontend/src/pages/Account/index.ts delete mode 100644 packages/frontend/src/pages/Analytics/Analytics.tsx delete mode 100644 packages/frontend/src/pages/Analytics/AnalyticsDashboard.js delete mode 100644 packages/frontend/src/pages/Analytics/Charts/BrowserTable.tsx delete mode 100644 packages/frontend/src/pages/Analytics/Charts/CountryTable.js delete mode 100644 packages/frontend/src/pages/Analytics/Charts/DeviceTable.js delete mode 100644 packages/frontend/src/pages/Analytics/Charts/OsTable.js delete mode 100644 packages/frontend/src/pages/Analytics/Charts/PageTable.js delete mode 100644 packages/frontend/src/pages/Analytics/Charts/ReferrerTable.js delete mode 100644 packages/frontend/src/pages/Analytics/Charts/Stats.js delete mode 100644 packages/frontend/src/pages/Analytics/Charts/TimeseriesChart.js delete mode 100644 packages/frontend/src/pages/Analytics/index.js delete mode 100644 packages/frontend/src/pages/EditWebsite/EditWebsite.js delete mode 100644 packages/frontend/src/pages/EditWebsite/index.js delete mode 100644 packages/frontend/src/pages/NewWebsite/NewWebsite.tsx delete mode 100644 packages/frontend/src/pages/NewWebsite/index.ts delete mode 100644 packages/frontend/src/pages/NotFound.tsx delete mode 100644 packages/frontend/src/pages/Setup/Setup.tsx delete mode 100644 packages/frontend/src/pages/Setup/SetupForm.tsx delete mode 100644 packages/frontend/src/pages/Setup/index.ts delete mode 100644 packages/frontend/src/pages/SignIn/SignIn.tsx delete mode 100644 packages/frontend/src/pages/SignIn/SignInForm.tsx delete mode 100644 packages/frontend/src/pages/SignIn/index.ts delete mode 100644 packages/frontend/src/pages/Websites/Websites.tsx delete mode 100644 packages/frontend/src/pages/Websites/WebsitesList.tsx delete mode 100644 packages/frontend/src/pages/Websites/index.ts delete mode 100644 packages/frontend/src/setupTests.js delete mode 100644 packages/frontend/src/types.ts delete mode 100644 packages/frontend/tsconfig.json create mode 100644 packages/tracker/package.json rename packages/{backend/lib/tracker => tracker/src}/aurora.js (100%) create mode 100644 pnpm-lock.yaml create mode 100644 pnpm-workspace.yaml diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 00000000..dd7bd43d --- /dev/null +++ b/.dockerignore @@ -0,0 +1,10 @@ +node_modules +**/node_modules +**/build +**/.next +**/.react-router +**/app/generated +.git +.github +*.md +**/.env diff --git a/.github/workflows/backend.yml b/.github/workflows/backend.yml deleted file mode 100644 index 8edd8c86..00000000 --- a/.github/workflows/backend.yml +++ /dev/null @@ -1,54 +0,0 @@ -name: Backend CI - -on: - push: - branches: [main] - pull_request: - branches: [main] - -jobs: - build: - runs-on: ubuntu-latest - defaults: - run: - working-directory: "./packages/backend" - - strategy: - matrix: - node-version: [12.x, 14.x, 16.x] - - services: - # Label used to access the service container - postgres: - # Docker Hub image - image: postgres - # Provide the password for postgres - env: - POSTGRES_DB: aurora - POSTGRES_USER: root - POSTGRES_PASSWORD: password - # Set health checks to wait until postgres has started - options: >- - --health-cmd pg_isready --health-interval 10s --health-timeout 5s - --health-retries 5 - ports: - # Maps tcp port 5432 on service container to the host - - 5432:5432 - - steps: - - uses: actions/checkout@v3 - - name: Use Node.js ${{ matrix.node-version }} - uses: actions/setup-node@v3 - with: - node-version: ${{ matrix.node-version }} - cache: "npm" - cache-dependency-path: "./packages/backend/package-lock.json" - - run: npm ci - - run: npm run build --if-present - - run: npm run db:migrate - env: - DATABASE_URL: postgres://root:password@localhost:5432/aurora - - run: npm test - env: - JWT_SECRET: impossibletoguess - DATABASE_URL: postgres://root:password@localhost:5432/aurora diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 00000000..e8de57c0 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,69 @@ +name: CI + +on: + push: + branches: [main] + pull_request: + branches: [main] + +jobs: + web: + name: Web + runs-on: ubuntu-latest + defaults: + run: + working-directory: apps/web + + env: + DATABASE_URL: postgres://root:password@localhost:5432/aurora + SESSION_SECRET: impossibletoguess + + services: + postgres: + image: postgres:16 + env: + POSTGRES_DB: aurora + POSTGRES_USER: root + POSTGRES_PASSWORD: password + options: >- + --health-cmd pg_isready --health-interval 10s --health-timeout 5s + --health-retries 5 + ports: + - 5432:5432 + + steps: + - uses: actions/checkout@v4 + - uses: pnpm/action-setup@v4 + - uses: actions/setup-node@v4 + with: + node-version: 22 + cache: pnpm + + - run: pnpm install --frozen-lockfile + working-directory: . + + - run: pnpm prisma generate + - run: pnpm db:migrate + - run: pnpm typecheck + - run: pnpm test + - run: pnpm build + + docs: + name: Docs + runs-on: ubuntu-latest + defaults: + run: + working-directory: apps/docs + + steps: + - uses: actions/checkout@v4 + - uses: pnpm/action-setup@v4 + - uses: actions/setup-node@v4 + with: + node-version: 22 + cache: pnpm + + - run: pnpm install --frozen-lockfile + working-directory: . + + - run: pnpm build diff --git a/.github/workflows/frontend.yml b/.github/workflows/frontend.yml deleted file mode 100644 index abb6f229..00000000 --- a/.github/workflows/frontend.yml +++ /dev/null @@ -1,30 +0,0 @@ -name: Frontend CI - -on: - push: - branches: [main] - pull_request: - branches: [main] - -jobs: - build: - runs-on: ubuntu-latest - defaults: - run: - working-directory: "./packages/frontend" - - strategy: - matrix: - node-version: [12.x, 14.x, 16.x] - - steps: - - uses: actions/checkout@v3 - - name: Use Node.js ${{ matrix.node-version }} - uses: actions/setup-node@v3 - with: - node-version: ${{ matrix.node-version }} - cache: "npm" - cache-dependency-path: "./packages/frontend/package-lock.json" - - run: npm ci - - run: npm run build --if-present - - run: npm run test:ci diff --git a/.gitignore b/.gitignore index 8a392a11..69d74b01 100644 --- a/.gitignore +++ b/.gitignore @@ -1,19 +1,22 @@ # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. # dependencies -/node_modules -/.pnp +node_modules/ +.pnp .pnp.js # testing -/coverage +coverage/ -# next.js -/.next/ -/out/ +# builds +build/ +dist/ +.next/ +out/ +.react-router/ -# production -/build +# generated prisma client +app/generated/ # misc .DS_Store @@ -23,8 +26,10 @@ npm-debug.log* yarn-debug.log* yarn-error.log* +pnpm-debug.log* # local env files +.env .env.local .env.development.local .env.test.local @@ -32,4 +37,7 @@ yarn-error.log* # vercel .vercel -.env + +# agent scaffolding regenerated by next dev +apps/docs/AGENTS.md +apps/docs/CLAUDE.md diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 00000000..237bd694 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,36 @@ +# Build context is the repository root: the web app is a pnpm workspace member +# and needs the root lockfile plus packages/tracker to build. +FROM node:22-alpine AS base +ENV PNPM_HOME=/pnpm +ENV PATH=$PNPM_HOME:$PATH +RUN corepack enable +WORKDIR /app + +FROM base AS deps +COPY package.json pnpm-lock.yaml pnpm-workspace.yaml ./ +COPY apps/web/package.json apps/web/ +COPY apps/docs/package.json apps/docs/ +COPY packages/tracker/package.json packages/tracker/ +RUN pnpm install --frozen-lockfile --filter web... --filter tracker... + +FROM deps AS build +COPY . . +RUN pnpm --filter web build + +FROM base AS prod-deps +COPY package.json pnpm-lock.yaml pnpm-workspace.yaml ./ +COPY apps/web/package.json apps/web/ +COPY packages/tracker/package.json packages/tracker/ +RUN pnpm install --frozen-lockfile --prod --filter web... + +FROM base AS runner +ENV NODE_ENV=production +COPY --from=prod-deps /app/node_modules ./node_modules +COPY --from=prod-deps /app/apps/web/node_modules ./apps/web/node_modules +COPY --from=build /app/apps/web/build ./apps/web/build +COPY --from=build /app/apps/web/package.json ./apps/web/package.json +COPY package.json pnpm-workspace.yaml ./ + +WORKDIR /app/apps/web +EXPOSE 3000 +CMD ["node", "./node_modules/@react-router/serve/bin.cjs", "./build/server/index.js"] diff --git a/README.md b/README.md index e075a280..6eb3325b 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -Aurora Logo +Aurora Logo
@@ -10,23 +10,50 @@ Hate Cookies? Introducing Aurora, 100% Cookie-Free Open Website Analytics. Collect Anonymous Data. Make your Audience Happy Now! -## Can I see a demo? 👀 +### Getting Started 🤩 -You can see a **running demo** -[here](https://aurora-app-frontend.vercel.app/websites/cl2re5iw6000809l843fb61jr/s/analytics)! +```bash +git clone https://github.com/itsrennyman/aurora +cd aurora +pnpm install -### Getting Started 🤩 +cp apps/web/.env.example apps/web/.env # set DATABASE_URL and SESSION_SECRET +pnpm db:migrate +pnpm dev +``` + +Then open `/setup` to create the first user. Full instructions live in the +[docs](apps/docs). + +### Repository Layout 📦 -You can go up and running with Aurora by following the -[Official Docs](https://aurora-docs.vercel.app). +pnpm workspace, one deployable app plus its docs: -> Please note that docs are currently under construction. +``` +apps/ + web React Router app — dashboard, /collect endpoint and tracker script + docs Nextra documentation site +packages/ + tracker Browser tracking script, bundled into apps/web/public/tracker.js +``` ### Built With 🏗️ -- [React.js](https://reactjs.org/) -- [Chakra UI](https://chakra-ui.com/) -- [Vercel](https://vercel.com/) +- [React Router](https://reactrouter.com/) (framework mode) +- [Tailwind CSS](https://tailwindcss.com/) and + [shadcn/ui](https://ui.shadcn.com/) +- [Prisma](https://www.prisma.io/) and PostgreSQL + +### Scripts 🛠️ + +| Command | Description | +| ----------------- | --------------------------------- | +| `pnpm dev` | Run the dashboard in development | +| `pnpm dev:docs` | Run the documentation site | +| `pnpm build` | Build every workspace package | +| `pnpm test` | Run the test suites | +| `pnpm typecheck` | Typecheck every workspace package | +| `pnpm db:migrate` | Apply database migrations | ### Versioning 🚦 diff --git a/apps/docs/.gitignore b/apps/docs/.gitignore new file mode 100644 index 00000000..36e65340 --- /dev/null +++ b/apps/docs/.gitignore @@ -0,0 +1,4 @@ +.DS_Store +node_modules/ +/.next/ +/out/ diff --git a/apps/docs/app/[[...mdxPath]]/page.jsx b/apps/docs/app/[[...mdxPath]]/page.jsx new file mode 100644 index 00000000..56364a6f --- /dev/null +++ b/apps/docs/app/[[...mdxPath]]/page.jsx @@ -0,0 +1,25 @@ +import { generateStaticParamsFor, importPage } from "nextra/pages"; +import { useMDXComponents } from "../../mdx-components"; + +export const generateStaticParams = generateStaticParamsFor("mdxPath"); + +export async function generateMetadata(props) { + const params = await props.params; + const { metadata } = await importPage(params.mdxPath); + + return metadata; +} + +const Wrapper = useMDXComponents().wrapper; + +export default async function Page(props) { + const params = await props.params; + const result = await importPage(params.mdxPath); + const { default: MDXContent, toc, metadata } = result; + + return ( + + + + ); +} diff --git a/apps/docs/app/layout.jsx b/apps/docs/app/layout.jsx new file mode 100644 index 00000000..f6c7edd7 --- /dev/null +++ b/apps/docs/app/layout.jsx @@ -0,0 +1,56 @@ +import { Footer, Layout, Navbar } from "nextra-theme-docs"; +import { getPageMap } from "nextra/page-map"; +import "nextra-theme-docs/style.css"; + +/** + * Nextra's component fails to render under Next 16, so the head tags it + * would emit are declared through the App Router metadata export instead. + */ +export const metadata = { + title: { default: "Aurora", template: "%s – Aurora" }, + description: "Aurora - Open Website Analytics", + icons: { icon: "/aurora.svg" }, +}; + +export const viewport = { + themeColor: [ + { media: "(prefers-color-scheme: light)", color: "#fafafa" }, + { media: "(prefers-color-scheme: dark)", color: "#111111" }, + ], +}; + +const AuroraLogo = () => ( + + + Aurora + +); + +export default async function RootLayout({ children }) { + return ( + + + } + projectLink="https://github.com/itsrennyman/aurora" + /> + } + footer={ +
MIT {new Date().getFullYear()} © Renato Pozzi.
+ } + > + {children} +
+ + + ); +} diff --git a/apps/docs/content/_meta.js b/apps/docs/content/_meta.js new file mode 100644 index 00000000..c182be0f --- /dev/null +++ b/apps/docs/content/_meta.js @@ -0,0 +1,4 @@ +export default { + index: "Introduction", + "get-started": "Get Started", +}; diff --git a/apps/docs/content/get-started.mdx b/apps/docs/content/get-started.mdx new file mode 100644 index 00000000..330ad883 --- /dev/null +++ b/apps/docs/content/get-started.mdx @@ -0,0 +1,116 @@ +--- +title: Getting Started +--- + +import { Callout, Steps } from "nextra/components"; + +# Getting Started + +Aurora is a single React Router application: the dashboard, the collection +endpoint and the tracker script are all served from one origin. There is no +separate backend service to deploy any more. + +## Requirements + +- Node.js 20 or newer, and [pnpm](https://pnpm.io/). +- A PostgreSQL instance to be used as data storage. + + + +### Clone and install + +```bash +git clone https://github.com/itsrennyman/aurora +cd aurora +pnpm install +``` + +### Configure the environment + +Copy the example file and fill it in: + +```bash +cp apps/web/.env.example apps/web/.env +``` + +| Variable | Description | +| ---------------- | ---------------------------------------------- | +| `DATABASE_URL` | Connection string of your PostgreSQL instance. | +| `SESSION_SECRET` | Secret used to sign the session cookie. | + +Generate a strong secret with: + +```bash +openssl rand -base64 32 +``` + + + `SESSION_SECRET` signs the cookie that keeps users signed in. Changing it + signs everyone out; leaking it lets anyone forge a session. + + +### Run the database migrations + +```bash +pnpm db:migrate +``` + +### Start it up + +```bash +pnpm dev +``` + +The dashboard is now available at `http://localhost:5173`. + +### Create the first user + +Open `/setup` and fill in the form. You will be signed in automatically. + + + The setup page is only available while the instance has no users. Once the + first account exists, both the page and the underlying action refuse any + further attempt. + + + + +## Connecting a website + +Create a website from the dashboard, then open its detail page. It shows a +snippet like this — copy it into your site's ``: + +```html + +``` + +The tracker derives the collection endpoint from its own `src`, so it always +reports back to the instance that served it. + +## Sharing statistics + +Enable **Share Statistics** on a website to expose a read-only dashboard at +`/websites//s/analytics`. While the flag is off, that URL requires the +owner's session. + +## Deploying + +Any host that runs a Node server works. Build once, then start: + +```bash +pnpm build +pnpm --filter web start +``` + +Migrations are applied separately with `pnpm db:migrate`, using the same +`DATABASE_URL`. + + + A `Dockerfile` at the repository root builds the app as a container. Build it + from the repository root, since the app is a workspace member. + diff --git a/apps/docs/content/index.mdx b/apps/docs/content/index.mdx new file mode 100644 index 00000000..5402969f --- /dev/null +++ b/apps/docs/content/index.mdx @@ -0,0 +1,34 @@ +--- +title: Introduction +--- + +# Introduction + +Hate Cookies? Introducing Aurora, 100% Cookie-Free Open Website Analytics. +Collect Anonymous Data. Make your Audience Happy Now! + +## Getting Started 🤩 + +Head over to [Get Started](/get-started) to deploy your own instance. + +## Built With 🏗️ + +- [React Router](https://reactrouter.com/) (framework mode) +- [Tailwind CSS](https://tailwindcss.com/) and + [shadcn/ui](https://ui.shadcn.com/) +- [Prisma](https://www.prisma.io/) and PostgreSQL + +## Versioning 🚦 + +We use [SemVer](http://semver.org/) for versioning. For the versions available, +see the [tags on this repository](https://github.com/itsrennyman/aurora/tags). + +## Authors 🙋 + +- [Renato Pozzi](https://github.com/itsrennyman) + +## License + +This project is licensed under the MIT License - see the +[LICENSE.md](https://github.com/itsrennyman/aurora/blob/main/LICENSE.md) file +for details. diff --git a/apps/docs/mdx-components.js b/apps/docs/mdx-components.js new file mode 100644 index 00000000..6cf7ef51 --- /dev/null +++ b/apps/docs/mdx-components.js @@ -0,0 +1,7 @@ +import { useMDXComponents as getThemeComponents } from "nextra-theme-docs"; + +const themeComponents = getThemeComponents(); + +export function useMDXComponents(components) { + return { ...themeComponents, ...components }; +} diff --git a/apps/docs/next.config.mjs b/apps/docs/next.config.mjs new file mode 100644 index 00000000..b2629333 --- /dev/null +++ b/apps/docs/next.config.mjs @@ -0,0 +1,9 @@ +import nextra from "nextra"; + +const withNextra = nextra({ + search: true, +}); + +export default withNextra({ + reactStrictMode: true, +}); diff --git a/packages/docs/package.json b/apps/docs/package.json similarity index 53% rename from packages/docs/package.json rename to apps/docs/package.json index 770e7866..c363b3e8 100644 --- a/packages/docs/package.json +++ b/apps/docs/package.json @@ -1,6 +1,7 @@ { "name": "docs", - "version": "2.0.1", + "private": true, + "version": "3.0.0", "description": "Aurora's Docs", "author": "Renato Pozzi ", "license": "MIT", @@ -11,18 +12,13 @@ "scripts": { "dev": "next dev", "build": "next build", - "start": "next start", - "lint": "next lint" + "start": "next start" }, "dependencies": { - "next": "12.1.5", - "nextra": "^1.1.0", - "nextra-theme-docs": "^1.2.6", - "react": "18.1.0", - "react-dom": "18.1.0" - }, - "devDependencies": { - "eslint": "8.14.0", - "eslint-config-next": "12.1.5" + "next": "^16.3.0", + "nextra": "^4.6.0", + "nextra-theme-docs": "^4.6.0", + "react": "^19.2.7", + "react-dom": "^19.2.7" } } diff --git a/packages/docs/public/aurora.svg b/apps/docs/public/aurora.svg similarity index 100% rename from packages/docs/public/aurora.svg rename to apps/docs/public/aurora.svg diff --git a/packages/docs/public/favicon.ico b/apps/docs/public/favicon.ico similarity index 100% rename from packages/docs/public/favicon.ico rename to apps/docs/public/favicon.ico diff --git a/apps/web/.env.example b/apps/web/.env.example new file mode 100644 index 00000000..a2522cd6 --- /dev/null +++ b/apps/web/.env.example @@ -0,0 +1,5 @@ +# Postgres connection string used by Prisma (runtime + migrations). +DATABASE_URL=postgres://root:password@localhost:5432/aurora + +# Secret used to sign the session cookie. Generate with: openssl rand -base64 32 +SESSION_SECRET=change-me diff --git a/apps/web/.gitignore b/apps/web/.gitignore new file mode 100644 index 00000000..4cf6196d --- /dev/null +++ b/apps/web/.gitignore @@ -0,0 +1,13 @@ +.DS_Store +.env +node_modules/ + +# React Router +/.react-router/ +/build/ + +# Prisma client is generated from schema.prisma +/app/generated/ + +# Built by packages/tracker +/public/tracker.js diff --git a/apps/web/README.md b/apps/web/README.md new file mode 100644 index 00000000..be425963 --- /dev/null +++ b/apps/web/README.md @@ -0,0 +1,92 @@ +# Welcome to React Router! + +A modern, production-ready template for building full-stack React applications +using React Router. + +[![Open in StackBlitz](https://developer.stackblitz.com/img/open_in_stackblitz.svg)](https://stackblitz.com/github/remix-run/react-router-templates/tree/main/default) + +## Features + +- 🚀 Server-side rendering +- ⚡️ Hot Module Replacement (HMR) +- 📦 Asset bundling and optimization +- 🔄 Data loading and mutations +- 🔒 TypeScript by default +- 🎉 TailwindCSS for styling +- 📖 [React Router docs](https://reactrouter.com/) + +## Getting Started + +### Installation + +Install the dependencies: + +```bash +npm install +``` + +### Development + +Start the development server with HMR: + +```bash +npm run dev +``` + +Your application will be available at `http://localhost:5173`. + +## Building for Production + +Create a production build: + +```bash +npm run build +``` + +## Deployment + +### Docker Deployment + +To build and run using Docker: + +```bash +docker build -t my-app . + +# Run the container +docker run -p 3000:3000 my-app +``` + +The containerized application can be deployed to any platform that supports +Docker, including: + +- AWS ECS +- Google Cloud Run +- Azure Container Apps +- Digital Ocean App Platform +- Fly.io +- Railway + +### DIY Deployment + +If you're familiar with deploying Node applications, the built-in app server is +production-ready. + +Make sure to deploy the output of `npm run build` + +``` +├── package.json +├── package-lock.json (or pnpm-lock.yaml, or bun.lockb) +├── build/ +│ ├── client/ # Static assets +│ └── server/ # Server-side code +``` + +## Styling + +This template comes with [Tailwind CSS](https://tailwindcss.com/) already +configured for a simple default starting experience. You can use whatever CSS +framework you prefer. + +--- + +Built with ❤️ using React Router. diff --git a/apps/web/app/app.css b/apps/web/app/app.css new file mode 100644 index 00000000..e49f238d --- /dev/null +++ b/apps/web/app/app.css @@ -0,0 +1,130 @@ +@import "tailwindcss"; +@import "tw-animate-css"; +@import "shadcn/tailwind.css"; +@import "@fontsource-variable/geist"; + +@custom-variant dark (&:is(.dark *)); + +@theme inline { + --font-heading: var(--font-sans); + --font-sans: "Geist Variable", sans-serif; + --color-sidebar-ring: var(--sidebar-ring); + --color-sidebar-border: var(--sidebar-border); + --color-sidebar-accent-foreground: var(--sidebar-accent-foreground); + --color-sidebar-accent: var(--sidebar-accent); + --color-sidebar-primary-foreground: var(--sidebar-primary-foreground); + --color-sidebar-primary: var(--sidebar-primary); + --color-sidebar-foreground: var(--sidebar-foreground); + --color-sidebar: var(--sidebar); + --color-chart-5: var(--chart-5); + --color-chart-4: var(--chart-4); + --color-chart-3: var(--chart-3); + --color-chart-2: var(--chart-2); + --color-chart-1: var(--chart-1); + --color-ring: var(--ring); + --color-input: var(--input); + --color-border: var(--border); + --color-destructive: var(--destructive); + --color-accent-foreground: var(--accent-foreground); + --color-accent: var(--accent); + --color-muted-foreground: var(--muted-foreground); + --color-muted: var(--muted); + --color-secondary-foreground: var(--secondary-foreground); + --color-secondary: var(--secondary); + --color-primary-foreground: var(--primary-foreground); + --color-primary: var(--primary); + --color-popover-foreground: var(--popover-foreground); + --color-popover: var(--popover); + --color-card-foreground: var(--card-foreground); + --color-card: var(--card); + --color-foreground: var(--foreground); + --color-background: var(--background); + --radius-sm: calc(var(--radius) * 0.6); + --radius-md: calc(var(--radius) * 0.8); + --radius-lg: var(--radius); + --radius-xl: calc(var(--radius) * 1.4); + --radius-2xl: calc(var(--radius) * 1.8); + --radius-3xl: calc(var(--radius) * 2.2); + --radius-4xl: calc(var(--radius) * 2.6); +} + +:root { + --background: oklch(1 0 0); + --foreground: oklch(0.145 0 0); + --card: oklch(1 0 0); + --card-foreground: oklch(0.145 0 0); + --popover: oklch(1 0 0); + --popover-foreground: oklch(0.145 0 0); + --primary: oklch(0.205 0 0); + --primary-foreground: oklch(0.985 0 0); + --secondary: oklch(0.97 0 0); + --secondary-foreground: oklch(0.205 0 0); + --muted: oklch(0.97 0 0); + --muted-foreground: oklch(0.556 0 0); + --accent: oklch(0.97 0 0); + --accent-foreground: oklch(0.205 0 0); + --destructive: oklch(0.577 0.245 27.325); + --border: oklch(0.922 0 0); + --input: oklch(0.922 0 0); + --ring: oklch(0.708 0 0); + --chart-1: oklch(0.87 0 0); + --chart-2: oklch(0.556 0 0); + --chart-3: oklch(0.439 0 0); + --chart-4: oklch(0.371 0 0); + --chart-5: oklch(0.269 0 0); + --radius: 0.625rem; + --sidebar: oklch(0.985 0 0); + --sidebar-foreground: oklch(0.145 0 0); + --sidebar-primary: oklch(0.205 0 0); + --sidebar-primary-foreground: oklch(0.985 0 0); + --sidebar-accent: oklch(0.97 0 0); + --sidebar-accent-foreground: oklch(0.205 0 0); + --sidebar-border: oklch(0.922 0 0); + --sidebar-ring: oklch(0.708 0 0); +} + +.dark { + --background: oklch(0.145 0 0); + --foreground: oklch(0.985 0 0); + --card: oklch(0.205 0 0); + --card-foreground: oklch(0.985 0 0); + --popover: oklch(0.205 0 0); + --popover-foreground: oklch(0.985 0 0); + --primary: oklch(0.922 0 0); + --primary-foreground: oklch(0.205 0 0); + --secondary: oklch(0.269 0 0); + --secondary-foreground: oklch(0.985 0 0); + --muted: oklch(0.269 0 0); + --muted-foreground: oklch(0.708 0 0); + --accent: oklch(0.269 0 0); + --accent-foreground: oklch(0.985 0 0); + --destructive: oklch(0.704 0.191 22.216); + --border: oklch(1 0 0 / 10%); + --input: oklch(1 0 0 / 15%); + --ring: oklch(0.556 0 0); + --chart-1: oklch(0.87 0 0); + --chart-2: oklch(0.556 0 0); + --chart-3: oklch(0.439 0 0); + --chart-4: oklch(0.371 0 0); + --chart-5: oklch(0.269 0 0); + --sidebar: oklch(0.205 0 0); + --sidebar-foreground: oklch(0.985 0 0); + --sidebar-primary: oklch(0.488 0.243 264.376); + --sidebar-primary-foreground: oklch(0.985 0 0); + --sidebar-accent: oklch(0.269 0 0); + --sidebar-accent-foreground: oklch(0.985 0 0); + --sidebar-border: oklch(1 0 0 / 10%); + --sidebar-ring: oklch(0.556 0 0); +} + +@layer base { + * { + @apply border-border outline-ring/50; + } + body { + @apply bg-background text-foreground; + } + html { + @apply font-sans; + } +} diff --git a/apps/web/app/components/analytics-dashboard.tsx b/apps/web/app/components/analytics-dashboard.tsx new file mode 100644 index 00000000..a18e6ba8 --- /dev/null +++ b/apps/web/app/components/analytics-dashboard.tsx @@ -0,0 +1,128 @@ +import { useEffect } from "react"; +import { useSearchParams } from "react-router"; +import { BreakdownTable } from "~/components/breakdown-table"; +import { TimeseriesChart } from "~/components/timeseries-chart"; +import { Card, CardContent, CardHeader, CardTitle } from "~/components/ui/card"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "~/components/ui/select"; +import type { BreakdownRow, Statistics, TimeseriesPoint } from "~/lib/types"; + +const RANGE_OPTIONS = [ + { value: "LAST_24_HOURS", label: "Last 24 Hours" }, + { value: "LAST_7_DAYS", label: "Last 7 Days" }, + { value: "LAST_30_DAYS", label: "Last 30 Days" }, +]; + +export type DashboardData = { + range: string; + stats: Statistics; + series: TimeseriesPoint[]; + breakdowns: { + pages: BreakdownRow[]; + referrers: BreakdownRow[]; + devices: BreakdownRow[]; + os: BreakdownRow[]; + browsers: BreakdownRow[]; + countries: BreakdownRow[]; + }; +}; + +function Stat({ label, value }: { label: string; value: string | number }) { + return ( + + + {label} + + +

{value}

+
+
+ ); +} + +export function AnalyticsDashboard({ data }: { data: DashboardData }) { + const [searchParams, setSearchParams] = useSearchParams(); + + // Bucketing is timezone-sensitive and the server can't know the visitor's + // zone, so the first client render pins it into the URL. + useEffect(() => { + if (searchParams.has("tz")) { + return; + } + + const tz = Intl.DateTimeFormat().resolvedOptions().timeZone; + + if (!tz) { + return; + } + + setSearchParams( + (prev) => { + prev.set("tz", tz); + return prev; + }, + { replace: true, preventScrollReset: true } + ); + }, [searchParams, setSearchParams]); + + const onRangeChange = (range: unknown) => { + setSearchParams( + (prev) => { + prev.set("range", String(range)); + return prev; + }, + { preventScrollReset: true } + ); + }; + + const { stats, breakdowns } = data; + + return ( +
+ + +
+ + + + +
+ + + +
+ + +
+ +
+ + +
+ +
+ + +
+
+ ); +} diff --git a/apps/web/app/components/breakdown-table.tsx b/apps/web/app/components/breakdown-table.tsx new file mode 100644 index 00000000..ed77a0d4 --- /dev/null +++ b/apps/web/app/components/breakdown-table.tsx @@ -0,0 +1,42 @@ +import { Fragment } from "react"; +import { Card, CardContent, CardHeader, CardTitle } from "~/components/ui/card"; +import type { BreakdownRow } from "~/lib/types"; + +export function BreakdownTable({ + title, + rows, +}: { + title: string; + rows: BreakdownRow[]; +}) { + return ( + + + {title} + + + {rows.length === 0 ? ( +

No Data Available

+ ) : ( +
+
Name
+
Views
+
Unique
+ +
+ + {rows.map((row) => ( + +
+ {row.element} +
+
{row.views}
+
{row.unique}
+
+ ))} +
+ )} + + + ); +} diff --git a/apps/web/app/components/footer.tsx b/apps/web/app/components/footer.tsx new file mode 100644 index 00000000..a4473bbc --- /dev/null +++ b/apps/web/app/components/footer.tsx @@ -0,0 +1,15 @@ +import { Logo } from "~/components/logo"; + +export function Footer() { + return ( +
+ + +

+ © {new Date().getFullYear()} Aurora, Open Web Analytics. +
+ All rights reserved. +

+
+ ); +} diff --git a/apps/web/app/components/logo.tsx b/apps/web/app/components/logo.tsx new file mode 100644 index 00000000..cba49dda --- /dev/null +++ b/apps/web/app/components/logo.tsx @@ -0,0 +1,18 @@ +type LogoProps = React.SVGProps; + +export function Logo(props: LogoProps) { + return ( + + + + ); +} diff --git a/apps/web/app/components/navbar.tsx b/apps/web/app/components/navbar.tsx new file mode 100644 index 00000000..f3d56559 --- /dev/null +++ b/apps/web/app/components/navbar.tsx @@ -0,0 +1,56 @@ +import { Home, LogOut, User } from "lucide-react"; +import { Form, Link } from "react-router"; +import { Logo } from "~/components/logo"; +import { ThemeToggle } from "~/components/theme-toggle"; +import { Button } from "~/components/ui/button"; + +function NavbarLink({ + to, + icon, + label, +}: { + to: string; + icon: React.ReactNode; + label: string; +}) { + return ( + + ); +} + +export function Navbar({ isPublic = false }: { isPublic?: boolean }) { + return ( + + ); +} diff --git a/apps/web/app/components/page-header.tsx b/apps/web/app/components/page-header.tsx new file mode 100644 index 00000000..646b9ad6 --- /dev/null +++ b/apps/web/app/components/page-header.tsx @@ -0,0 +1,31 @@ +import { cn } from "~/lib/utils"; + +export function Page({ + children, + className, +}: { + children: React.ReactNode; + className?: string; +}) { + return ( +
+ {children} +
+ ); +} + +export function PageHeader({ children }: { children: React.ReactNode }) { + return ( +
+ {children} +
+ ); +} + +export function PageTitle({ children }: { children: React.ReactNode }) { + return

{children}

; +} + +export function PageActions({ children }: { children: React.ReactNode }) { + return
{children}
; +} diff --git a/apps/web/app/components/shell.tsx b/apps/web/app/components/shell.tsx new file mode 100644 index 00000000..fe639f7e --- /dev/null +++ b/apps/web/app/components/shell.tsx @@ -0,0 +1,21 @@ +import { Footer } from "~/components/footer"; +import { Navbar } from "~/components/navbar"; + +export function Shell({ + children, + isPublic = false, +}: { + children: React.ReactNode; + isPublic?: boolean; +}) { + return ( + <> + + +
+ {children} +
+
+ + ); +} diff --git a/apps/web/app/components/theme-toggle.tsx b/apps/web/app/components/theme-toggle.tsx new file mode 100644 index 00000000..95d264ea --- /dev/null +++ b/apps/web/app/components/theme-toggle.tsx @@ -0,0 +1,37 @@ +import { Moon, Sun } from "lucide-react"; +import { useEffect, useState } from "react"; +import { Button } from "~/components/ui/button"; + +/** + * The initial class is applied by the inline script in root.tsx; this only + * reflects and flips it. `mounted` keeps the server render icon-free so the + * markup matches before hydration. + */ +export function ThemeToggle() { + const [isDark, setIsDark] = useState(false); + const [mounted, setMounted] = useState(false); + + useEffect(() => { + setIsDark(document.documentElement.classList.contains("dark")); + setMounted(true); + }, []); + + const toggle = () => { + const next = !isDark; + + document.documentElement.classList.toggle("dark", next); + localStorage.setItem("aurora-theme", next ? "dark" : "light"); + setIsDark(next); + }; + + return ( + + ); +} diff --git a/apps/web/app/components/timeseries-chart.tsx b/apps/web/app/components/timeseries-chart.tsx new file mode 100644 index 00000000..e73dfda5 --- /dev/null +++ b/apps/web/app/components/timeseries-chart.tsx @@ -0,0 +1,82 @@ +import { useEffect, useState } from "react"; +import Chart from "react-apexcharts"; +import { Card, CardContent, CardHeader, CardTitle } from "~/components/ui/card"; +import { Skeleton } from "~/components/ui/skeleton"; +import type { TimeseriesPoint } from "~/lib/types"; + +/** + * ApexCharts touches `window` at module scope, so the chart only mounts after + * hydration. The card renders a skeleton of the same height server-side to keep + * the layout from jumping. + */ +export function TimeseriesChart({ data }: { data: TimeseriesPoint[] }) { + const [mounted, setMounted] = useState(false); + const [isDark, setIsDark] = useState(false); + + useEffect(() => { + setMounted(true); + + const root = document.documentElement; + const sync = () => setIsDark(root.classList.contains("dark")); + + sync(); + + const observer = new MutationObserver(sync); + observer.observe(root, { attributes: true, attributeFilter: ["class"] }); + + return () => observer.disconnect(); + }, []); + + return ( + + + Number of Page Visits + + + {!mounted ? ( + + ) : ( + d.count) }]} + options={{ + chart: { + fontFamily: "inherit", + parentHeightOffset: 0, + toolbar: { show: false }, + animations: { enabled: true }, + stacked: true, + foreColor: isDark ? "#fff" : "#000", + zoom: { enabled: false }, + background: "transparent", + }, + plotOptions: { bar: { columnWidth: "80%" } }, + dataLabels: { enabled: false }, + fill: { opacity: 1 }, + grid: { + padding: { top: -20, right: 0, left: -4, bottom: 20 }, + strokeDashArray: 4, + xaxis: { lines: { show: true } }, + }, + xaxis: { + type: "datetime", + labels: { datetimeUTC: false }, + tooltip: { enabled: false }, + axisBorder: { show: false }, + }, + tooltip: { enabled: true, followCursor: false }, + labels: data.map((d) => d.timeseries), + colors: [isDark ? "#bfe399" : "#555de3"], + legend: { + show: true, + fontSize: "16px", + itemMargin: { horizontal: 10, vertical: 0 }, + }, + }} + /> + )} + + + ); +} diff --git a/apps/web/app/components/ui/alert-dialog.tsx b/apps/web/app/components/ui/alert-dialog.tsx new file mode 100644 index 00000000..3e1240d1 --- /dev/null +++ b/apps/web/app/components/ui/alert-dialog.tsx @@ -0,0 +1,185 @@ +import * as React from "react"; +import { AlertDialog as AlertDialogPrimitive } from "@base-ui/react/alert-dialog"; + +import { cn } from "~/lib/utils"; +import { Button } from "~/components/ui/button"; + +function AlertDialog({ ...props }: AlertDialogPrimitive.Root.Props) { + return ; +} + +function AlertDialogTrigger({ ...props }: AlertDialogPrimitive.Trigger.Props) { + return ( + + ); +} + +function AlertDialogPortal({ ...props }: AlertDialogPrimitive.Portal.Props) { + return ( + + ); +} + +function AlertDialogOverlay({ + className, + ...props +}: AlertDialogPrimitive.Backdrop.Props) { + return ( + + ); +} + +function AlertDialogContent({ + className, + size = "default", + ...props +}: AlertDialogPrimitive.Popup.Props & { + size?: "default" | "sm"; +}) { + return ( + + + + + ); +} + +function AlertDialogHeader({ + className, + ...props +}: React.ComponentProps<"div">) { + return ( +
+ ); +} + +function AlertDialogFooter({ + className, + ...props +}: React.ComponentProps<"div">) { + return ( +
+ ); +} + +function AlertDialogMedia({ + className, + ...props +}: React.ComponentProps<"div">) { + return ( +
+ ); +} + +function AlertDialogTitle({ + className, + ...props +}: React.ComponentProps) { + return ( + + ); +} + +function AlertDialogDescription({ + className, + ...props +}: React.ComponentProps) { + return ( + + ); +} + +function AlertDialogAction({ + className, + ...props +}: React.ComponentProps) { + return ( +