@carlonicora/nextjs-jsonapi 3.24.2 → 3.25.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (178) hide show
  1. package/dist/{AssistantMessageInterface-1Kbz8RVs.d.ts → AssistantMessageInterface-CubxuBIJ.d.ts} +1 -1
  2. package/dist/{AssistantMessageInterface-CoxJyuFM.d.mts → AssistantMessageInterface-sHjI2zPN.d.mts} +1 -1
  3. package/dist/{BlockNoteEditor-65HR6ULV.mjs → BlockNoteEditor-46O3JC4Z.mjs} +5 -5
  4. package/dist/{BlockNoteEditor-35D4R7HJ.js → BlockNoteEditor-HKS6AXDA.js} +20 -20
  5. package/dist/{BlockNoteEditor-35D4R7HJ.js.map → BlockNoteEditor-HKS6AXDA.js.map} +1 -1
  6. package/dist/{BlockNoteViewer-QDXDVYEM.js → BlockNoteViewer-7BFNTN4E.js} +4 -4
  7. package/dist/{BlockNoteViewer-QDXDVYEM.js.map → BlockNoteViewer-7BFNTN4E.js.map} +1 -1
  8. package/dist/{BlockNoteViewer-KDKGUIG5.mjs → BlockNoteViewer-KRTAFTUX.mjs} +3 -3
  9. package/dist/{HandbookSectionInterface-OePKPbqN.d.ts → HandbookSectionInterface-Ck1wZJ81.d.ts} +1 -1
  10. package/dist/{HandbookSectionInterface-BYsmnPEz.d.mts → HandbookSectionInterface-DgYH--Ik.d.mts} +1 -1
  11. package/dist/JsonApiRequest-TE5WD3WS.js +26 -0
  12. package/dist/{JsonApiRequest-G625QAH5.js.map → JsonApiRequest-TE5WD3WS.js.map} +1 -1
  13. package/dist/{JsonApiRequest-RRMNV7YE.mjs → JsonApiRequest-UHOWNI3O.mjs} +2 -2
  14. package/dist/analytics.modules-CfczU12P.d.ts +25 -0
  15. package/dist/analytics.modules-pzGwMASI.d.mts +25 -0
  16. package/dist/{auth.interface-BC1jp_ni.d.ts → auth.interface-BzI4onI_.d.ts} +1 -1
  17. package/dist/{auth.interface-dR2-6gPM.d.mts → auth.interface-Df4vXbNz.d.mts} +1 -1
  18. package/dist/billing/index.js +402 -402
  19. package/dist/billing/index.mjs +4 -4
  20. package/dist/{chunk-YXRVPEEJ.js → chunk-26MQIJNB.js} +1 -1
  21. package/dist/{chunk-YXRVPEEJ.js.map → chunk-26MQIJNB.js.map} +1 -1
  22. package/dist/{chunk-T76WLDE5.mjs → chunk-37DMFVLN.mjs} +1 -1
  23. package/dist/chunk-4Z2UONN2.mjs +110 -0
  24. package/dist/chunk-4Z2UONN2.mjs.map +1 -0
  25. package/dist/{chunk-3AR4KA5D.js → chunk-4Z45NYPR.js} +13 -13
  26. package/dist/{chunk-3AR4KA5D.js.map → chunk-4Z45NYPR.js.map} +1 -1
  27. package/dist/chunk-7B4E42R5.mjs +280 -0
  28. package/dist/chunk-7B4E42R5.mjs.map +1 -0
  29. package/dist/{chunk-U7QVISQS.js → chunk-CAKOFJZD.js} +450 -40
  30. package/dist/chunk-CAKOFJZD.js.map +1 -0
  31. package/dist/{chunk-IBVKPLHT.mjs → chunk-E3I4MT53.mjs} +422 -12
  32. package/dist/chunk-E3I4MT53.mjs.map +1 -0
  33. package/dist/chunk-GHFMKH3M.js +280 -0
  34. package/dist/chunk-GHFMKH3M.js.map +1 -0
  35. package/dist/chunk-KLV6RLDE.js +110 -0
  36. package/dist/chunk-KLV6RLDE.js.map +1 -0
  37. package/dist/{chunk-ZJI5ZZW5.mjs → chunk-Q6J2ICKO.mjs} +3 -3
  38. package/dist/{chunk-4O7R2Z2O.mjs → chunk-XDYILJ2S.mjs} +1749 -1737
  39. package/dist/{chunk-4O7R2Z2O.mjs.map → chunk-XDYILJ2S.mjs.map} +1 -1
  40. package/dist/{chunk-2XCUAHQT.js → chunk-Y3OKEQGL.js} +1126 -1114
  41. package/dist/chunk-Y3OKEQGL.js.map +1 -0
  42. package/dist/client/index.d.mts +6 -5
  43. package/dist/client/index.d.ts +6 -5
  44. package/dist/client/index.js +5 -5
  45. package/dist/client/index.js.map +1 -1
  46. package/dist/client/index.mjs +4 -4
  47. package/dist/components/index.d.mts +17 -7
  48. package/dist/components/index.d.ts +17 -7
  49. package/dist/components/index.js +5 -5
  50. package/dist/components/index.mjs +4 -4
  51. package/dist/{config-B6UB7j69.d.ts → config-3N1LSrcC.d.ts} +1 -1
  52. package/dist/{config-Cd5xTEfZ.d.ts → config-AVtuvfM1.d.ts} +1 -1
  53. package/dist/{config-n-ZpPmOL.d.mts → config-BSKTYDoh.d.mts} +1 -1
  54. package/dist/{config-YyhFHxkU.d.mts → config-BmufyEj6.d.mts} +1 -1
  55. package/dist/contexts/index.d.mts +5 -4
  56. package/dist/contexts/index.d.ts +5 -4
  57. package/dist/contexts/index.js +5 -5
  58. package/dist/contexts/index.mjs +4 -4
  59. package/dist/core/index.d.mts +31 -14
  60. package/dist/core/index.d.ts +31 -14
  61. package/dist/core/index.js +9 -5
  62. package/dist/core/index.js.map +1 -1
  63. package/dist/core/index.mjs +8 -4
  64. package/dist/features/analytics/index.css +30 -0
  65. package/dist/features/analytics/index.css.map +1 -0
  66. package/dist/features/analytics/index.d.mts +508 -0
  67. package/dist/features/analytics/index.d.ts +508 -0
  68. package/dist/features/analytics/index.js +1166 -0
  69. package/dist/features/analytics/index.js.map +1 -0
  70. package/dist/features/analytics/index.mjs +1166 -0
  71. package/dist/features/analytics/index.mjs.map +1 -0
  72. package/dist/features/analytics/server.d.mts +28 -0
  73. package/dist/features/analytics/server.d.ts +28 -0
  74. package/dist/features/analytics/server.js +41 -0
  75. package/dist/features/analytics/server.js.map +1 -0
  76. package/dist/features/analytics/server.mjs +41 -0
  77. package/dist/features/analytics/server.mjs.map +1 -0
  78. package/dist/features/help/index.d.mts +2 -2
  79. package/dist/features/help/index.d.ts +2 -2
  80. package/dist/features/help/index.js +38 -38
  81. package/dist/features/help/index.mjs +4 -4
  82. package/dist/features/tokenusage/index.d.mts +2 -2
  83. package/dist/features/tokenusage/index.d.ts +2 -2
  84. package/dist/features/tokenusage/index.js +139 -221
  85. package/dist/features/tokenusage/index.js.map +1 -1
  86. package/dist/features/tokenusage/index.mjs +26 -108
  87. package/dist/features/tokenusage/index.mjs.map +1 -1
  88. package/dist/index.d.mts +11 -9
  89. package/dist/index.d.ts +11 -9
  90. package/dist/index.js +10 -6
  91. package/dist/index.js.map +1 -1
  92. package/dist/index.mjs +9 -5
  93. package/dist/notification.interface-BNCvnVCW.d.ts +34 -0
  94. package/dist/notification.interface-P_pNfzZQ.d.mts +34 -0
  95. package/dist/{s3.service-Bqr-fQmi.d.ts → s3.service-CbUa6a-8.d.ts} +3 -2
  96. package/dist/{s3.service-Buiy1ncj.d.mts → s3.service-DAp8FbuG.d.mts} +3 -2
  97. package/dist/server/index.d.mts +5 -4
  98. package/dist/server/index.d.ts +5 -4
  99. package/dist/server/index.js +39 -279
  100. package/dist/server/index.js.map +1 -1
  101. package/dist/server/index.mjs +20 -260
  102. package/dist/server/index.mjs.map +1 -1
  103. package/dist/{types-bsm8Fp55.d.ts → types-Cn153-mH.d.ts} +1 -1
  104. package/dist/{types-Bq_UA8Lg.d.mts → types-D0uLYV2P.d.mts} +1 -1
  105. package/dist/{usePageUrlGenerator-J_ECv-oQ.d.ts → usePageUrlGenerator-B-3FRKBD.d.ts} +1 -1
  106. package/dist/{usePageUrlGenerator-uKBjAKqC.d.mts → usePageUrlGenerator-CCJ_cdQy.d.mts} +1 -1
  107. package/dist/{useSocket-BcH7l7B8.d.mts → useSocket-B9gTbuCj.d.mts} +1 -1
  108. package/dist/{useSocket-DCZd_eJk.d.ts → useSocket-BHVIyigI.d.ts} +1 -1
  109. package/dist/{notification.interface-MnRt1Tjm.d.mts → user.interface-CSaGWFbg.d.mts} +1 -31
  110. package/dist/{notification.interface-CjzV_KII.d.ts → user.interface-CrmwJLYW.d.ts} +1 -31
  111. package/package.json +11 -1
  112. package/src/client/index.ts +2 -0
  113. package/src/core/index.ts +4 -0
  114. package/src/core/registry/ModuleRegistry.ts +7 -0
  115. package/src/features/__tests__/i18n-keys.contract.spec.ts +1 -0
  116. package/src/features/analytics/__tests__/server-entry.spec.tsx +64 -0
  117. package/src/features/analytics/analytics.modules.ts +30 -0
  118. package/src/features/analytics/components/AnalyticsAdminContainer.tsx +97 -0
  119. package/src/features/analytics/components/AnalyticsAdminFilterBar.tsx +117 -0
  120. package/src/features/analytics/components/AnalyticsAdminPageContainer.tsx +25 -0
  121. package/src/features/analytics/components/AnalyticsAdminTiles.tsx +191 -0
  122. package/src/features/analytics/components/AnalyticsBreakdownTable.tsx +104 -0
  123. package/src/features/analytics/components/AnalyticsJourneyContainer.tsx +246 -0
  124. package/src/features/analytics/components/AnalyticsJourneyPageContainer.tsx +14 -0
  125. package/src/features/analytics/components/AnalyticsSessionsTable.tsx +107 -0
  126. package/src/features/analytics/components/AnalyticsTimelineChart.tsx +175 -0
  127. package/src/features/analytics/components/AnalyticsTracker.tsx +112 -0
  128. package/src/features/analytics/components/__tests__/AnalyticsAdminTiles.spec.tsx +39 -0
  129. package/src/features/analytics/components/__tests__/AnalyticsBreakdownTable.spec.tsx +39 -0
  130. package/src/features/analytics/components/__tests__/AnalyticsJourneyContainer.spec.tsx +124 -0
  131. package/src/features/analytics/components/__tests__/AnalyticsPageContainers.spec.tsx +117 -0
  132. package/src/features/analytics/components/__tests__/AnalyticsSessionsTable.spec.tsx +121 -0
  133. package/src/features/analytics/components/__tests__/AnalyticsTimelineChart.spec.tsx +35 -0
  134. package/src/features/analytics/components/__tests__/AnalyticsTracker.spec.tsx +248 -0
  135. package/src/features/analytics/components/server/AnalyticsAdminPage.tsx +28 -0
  136. package/src/features/analytics/components/server/AnalyticsJourneyPage.tsx +25 -0
  137. package/src/features/analytics/contexts/AnalyticsAdminContext.tsx +273 -0
  138. package/src/features/analytics/contexts/__tests__/AnalyticsAdminContext.spec.tsx +140 -0
  139. package/src/features/analytics/data/AnalyticsAdminService.ts +91 -0
  140. package/src/features/analytics/data/AnalyticsService.ts +23 -0
  141. package/src/features/analytics/data/__tests__/AnalyticsAdminService.spec.ts +117 -0
  142. package/src/features/analytics/data/__tests__/AnalyticsService.spec.ts +56 -0
  143. package/src/features/analytics/data/__tests__/models.spec.ts +300 -0
  144. package/src/features/analytics/data/analytics-breakdown.interface.ts +9 -0
  145. package/src/features/analytics/data/analytics-breakdown.ts +65 -0
  146. package/src/features/analytics/data/analytics-event.interface.ts +7 -0
  147. package/src/features/analytics/data/analytics-event.ts +63 -0
  148. package/src/features/analytics/data/analytics-page-view.interface.ts +9 -0
  149. package/src/features/analytics/data/analytics-page-view.ts +57 -0
  150. package/src/features/analytics/data/analytics-session.interface.ts +23 -0
  151. package/src/features/analytics/data/analytics-session.ts +144 -0
  152. package/src/features/analytics/data/analytics-summary.interface.ts +11 -0
  153. package/src/features/analytics/data/analytics-summary.ts +79 -0
  154. package/src/features/analytics/data/analytics-timeline.interface.ts +10 -0
  155. package/src/features/analytics/data/analytics-timeline.ts +67 -0
  156. package/src/features/analytics/data/analytics.types.ts +97 -0
  157. package/src/features/analytics/data/index.ts +15 -0
  158. package/src/features/analytics/i18n-keys.ts +76 -0
  159. package/src/features/analytics/index.ts +31 -0
  160. package/src/features/analytics/lib/__tests__/utm.spec.ts +20 -0
  161. package/src/features/analytics/lib/__tests__/visitorCookie.spec.ts +63 -0
  162. package/src/features/analytics/lib/utm.ts +29 -0
  163. package/src/features/analytics/lib/visitorCookie.ts +32 -0
  164. package/src/features/analytics/server-entry.ts +8 -0
  165. package/src/features/waitlist/components/lists/WaitlistList.tsx +68 -121
  166. package/src/features/waitlist/data/WaitlistService.ts +2 -2
  167. package/src/features/waitlist/data/index.ts +1 -0
  168. package/src/features/waitlist/data/waitlist.fields.ts +8 -0
  169. package/src/features/waitlist/hooks/useWaitlistTableStructure.tsx +69 -30
  170. package/dist/JsonApiRequest-G625QAH5.js +0 -26
  171. package/dist/chunk-2XCUAHQT.js.map +0 -1
  172. package/dist/chunk-IBVKPLHT.mjs.map +0 -1
  173. package/dist/chunk-U7QVISQS.js.map +0 -1
  174. /package/dist/{BlockNoteEditor-65HR6ULV.mjs.map → BlockNoteEditor-46O3JC4Z.mjs.map} +0 -0
  175. /package/dist/{BlockNoteViewer-KDKGUIG5.mjs.map → BlockNoteViewer-KRTAFTUX.mjs.map} +0 -0
  176. /package/dist/{JsonApiRequest-RRMNV7YE.mjs.map → JsonApiRequest-UHOWNI3O.mjs.map} +0 -0
  177. /package/dist/{chunk-T76WLDE5.mjs.map → chunk-37DMFVLN.mjs.map} +0 -0
  178. /package/dist/{chunk-ZJI5ZZW5.mjs.map → chunk-Q6J2ICKO.mjs.map} +0 -0
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/server/cache.ts","../../src/server/ServerSession.ts","../../src/server/JsonApiServer.ts"],"sourcesContent":["// Server-only utilities (NOT server actions - these cannot be called from client components)\n\nimport { revalidateTag } from \"next/cache\";\n\nexport type CacheProfile = \"seconds\" | \"minutes\" | \"hours\" | \"days\" | \"weeks\" | \"max\" | \"default\";\n\n/**\n * Revalidate a cache tag to invalidate cached data.\n * Next.js 16+ requires a profile parameter.\n *\n * @param tag - The cache tag to invalidate\n * @param profile - The cache profile (defaults to \"max\" for immediate invalidation)\n */\nexport async function invalidateCacheTag(tag: string, profile: CacheProfile = \"max\"): Promise<void> {\n revalidateTag(tag, profile);\n}\n\n/**\n * Revalidate multiple cache tags.\n *\n * @param tags - Array of cache tags to invalidate\n * @param profile - The cache profile (defaults to \"max\" for immediate invalidation)\n */\nexport async function invalidateCacheTags(tags: string[], profile: CacheProfile = \"max\"): Promise<void> {\n for (const tag of tags) {\n revalidateTag(tag, profile);\n }\n}\n","import { cookies } from \"next/headers\";\nimport { redirect } from \"next/navigation\";\nimport { ungzip } from \"pako\";\nimport { Action, checkPermissionsFromServer, ModuleWithPermissions } from \"../permissions\";\n\nexport class ServerSession {\n static async isLogged() {\n const cookieStore = await cookies();\n const token = cookieStore.get(\"token\");\n\n if (!token || !token.value) return false;\n return true;\n }\n\n static async companyId() {\n const cookieStore = await cookies();\n return cookieStore.get(\"companyId\")?.value ?? \"\";\n }\n\n static async userId() {\n const cookieStore = await cookies();\n return cookieStore.get(\"userId\")?.value ?? \"\";\n }\n\n static async checkPermission<M extends ModuleWithPermissions>(params: { module: M; action: Action; data?: any }) {\n if (!(await this.hasPermissionToModule(params))) redirect(`/401`);\n }\n\n static async hasRole(roleId: string): Promise<boolean> {\n const cookieStore = await cookies();\n const roles = cookieStore.get(\"roles\")?.value;\n\n if (!roles || !roles.includes(roleId)) return false;\n\n return true;\n }\n\n static async hasFeature(featureId: string): Promise<boolean> {\n const cookieStore = await cookies();\n const features = cookieStore.get(\"features\")?.value;\n\n if (!features || !features.includes(featureId)) return false;\n\n return true;\n }\n\n static async hasPermissionToModule<M extends ModuleWithPermissions>(params: {\n module: M;\n action: Action;\n data?: any;\n }): Promise<boolean> {\n const cookieStore = await cookies();\n\n if (params.module.feature) {\n const features = cookieStore.get(\"features\")?.value;\n\n if (features && !features.includes(params.module.feature)) return false;\n }\n\n const rawModules = cookieStore.get(\"modules\")?.value;\n if (!rawModules) return false;\n\n const modules: {\n id: string;\n permissions: {\n create: boolean | string;\n read: boolean | string;\n update: boolean | string;\n delete: boolean | string;\n };\n }[] = JSON.parse(ungzip(Buffer.from(rawModules, \"base64\"), { toText: true }));\n\n const selectedModule = modules.find((module) => module.id === params.module.moduleId);\n\n return checkPermissionsFromServer({\n module: params.module,\n action: params.action,\n data: params.data,\n userId: await this.userId(),\n selectedModule: selectedModule,\n });\n\n // if (!selectedModule) return false;\n\n // if (!selectedModule.permissions[params.action]) return false;\n // if (typeof selectedModule.permissions[params.action] === \"boolean\")\n // return selectedModule.permissions[params.action] as boolean;\n\n // if (!params.data) return false;\n\n // return params.data[selectedModule.permissions[params.action] as string] === this.userId;\n }\n}\n","// Server-only utilities (NOT server actions - these cannot be called from client components)\n\nimport { ENV } from \"../core/env\";\nimport { JsonApiDataFactory } from \"../core/factories/JsonApiDataFactory\";\nimport { ApiRequestDataTypeInterface } from \"../core/interfaces/ApiRequestDataTypeInterface\";\nimport { ApiResponseInterface } from \"../core/interfaces/ApiResponseInterface\";\nimport { translateResponse } from \"../core/utils/translateResponse\";\nimport { ModuleWithPermissions } from \"../permissions/types\";\nimport { serverRequest } from \"./request\";\nimport { getServerToken } from \"./token\";\n\n// Config storage for server contexts\nlet _serverConfig: {\n apiUrl: string;\n appUrl?: string;\n trackablePages?: ModuleWithPermissions[];\n bootstrapper?: () => void;\n additionalHeaders?: Record<string, string>;\n} | null = null;\n\n/**\n * Configure the JSON:API server client.\n * Call this in your Bootstrapper or server initialization.\n */\nexport function configureServerJsonApi(config: {\n apiUrl: string;\n appUrl?: string;\n trackablePages?: ModuleWithPermissions[];\n bootstrapper?: () => void;\n additionalHeaders?: Record<string, string>;\n}): void {\n _serverConfig = config;\n if (config.bootstrapper) {\n config.bootstrapper();\n }\n}\n\nexport function getServerApiUrl(): string {\n if (_serverConfig?.apiUrl) {\n return _serverConfig.apiUrl;\n }\n const envUrl = ENV.API_URL;\n if (!envUrl) {\n throw new Error(\n \"API URL not configured. Use configureServerJsonApi() or set NEXT_PUBLIC_API_URL environment variable.\",\n );\n }\n return envUrl;\n}\n\nexport function getServerAppUrl(): string {\n if (_serverConfig?.appUrl) {\n return _serverConfig.appUrl;\n }\n const envUrl = ENV.APP_URL;\n if (!envUrl) {\n throw new Error(\n \"App URL not configured. Use configureServerJsonApi({ appUrl }) or set NEXT_PUBLIC_ADDRESS environment variable.\",\n );\n }\n return envUrl.trim().replace(/\\/+$/, \"\");\n}\n\nexport function getServerTrackablePages(): ModuleWithPermissions[] {\n return _serverConfig?.trackablePages ?? [];\n}\n\nfunction runServerBootstrapper(): void {\n if (_serverConfig?.bootstrapper) {\n _serverConfig.bootstrapper();\n }\n}\n\nfunction buildServerUrl(endpoint: string): string {\n const apiUrl = getServerApiUrl();\n return endpoint.startsWith(\"http\") ? endpoint : `${apiUrl}${endpoint}`;\n}\n\nexport async function ServerJsonApiGet(params: {\n classKey: ApiRequestDataTypeInterface;\n endpoint: string;\n companyId?: string;\n language: string;\n}): Promise<ApiResponseInterface> {\n runServerBootstrapper();\n const token = await getServerToken();\n\n const apiResponse = await serverRequest({\n method: \"GET\",\n url: buildServerUrl(params.endpoint),\n token,\n cache: params.classKey.cache,\n companyId: params.companyId,\n language: params.language,\n additionalHeaders: _serverConfig?.additionalHeaders,\n });\n\n return translateResponse({\n classKey: params.classKey,\n apiResponse,\n companyId: params.companyId,\n language: params.language,\n paginationHandler: async (endpoint: string) =>\n ServerJsonApiGet({\n classKey: params.classKey,\n endpoint,\n companyId: params.companyId,\n language: params.language,\n }),\n });\n}\n\nexport async function ServerJsonApiPost(params: {\n classKey: ApiRequestDataTypeInterface;\n endpoint: string;\n companyId?: string;\n body?: any;\n overridesJsonApiCreation?: boolean;\n files?: { [key: string]: File | Blob } | File | Blob;\n language: string;\n responseType?: ApiRequestDataTypeInterface;\n}): Promise<ApiResponseInterface> {\n runServerBootstrapper();\n const token = await getServerToken();\n\n let body = params.body;\n if (!body) {\n body = {};\n } else if (params.overridesJsonApiCreation !== true) {\n body = JsonApiDataFactory.create(params.classKey, body);\n }\n\n const apiResponse = await serverRequest({\n method: \"POST\",\n url: buildServerUrl(params.endpoint),\n token,\n body,\n files: params.files,\n companyId: params.companyId,\n language: params.language,\n additionalHeaders: _serverConfig?.additionalHeaders,\n });\n\n return translateResponse({\n classKey: params.responseType ?? params.classKey,\n apiResponse,\n companyId: params.companyId,\n language: params.language,\n });\n}\n\nexport async function ServerJsonApiPut(params: {\n classKey: ApiRequestDataTypeInterface;\n endpoint: string;\n companyId?: string;\n body?: any;\n files?: { [key: string]: File | Blob } | File | Blob;\n language: string;\n responseType?: ApiRequestDataTypeInterface;\n}): Promise<ApiResponseInterface> {\n runServerBootstrapper();\n const token = await getServerToken();\n\n let body = params.body;\n if (!body) {\n body = {};\n } else {\n body = JsonApiDataFactory.create(params.classKey, body);\n }\n\n const apiResponse = await serverRequest({\n method: \"PUT\",\n url: buildServerUrl(params.endpoint),\n token,\n body,\n files: params.files,\n companyId: params.companyId,\n language: params.language,\n additionalHeaders: _serverConfig?.additionalHeaders,\n });\n\n return translateResponse({\n classKey: params.responseType ?? params.classKey,\n apiResponse,\n companyId: params.companyId,\n language: params.language,\n });\n}\n\nexport async function ServerJsonApiPatch(params: {\n classKey: ApiRequestDataTypeInterface;\n endpoint: string;\n companyId?: string;\n body?: any;\n files?: { [key: string]: File | Blob } | File | Blob;\n overridesJsonApiCreation?: boolean;\n responseType?: ApiRequestDataTypeInterface;\n language: string;\n}): Promise<ApiResponseInterface> {\n runServerBootstrapper();\n const token = await getServerToken();\n\n let body = params.body;\n if (!body) {\n body = {};\n } else if (params.overridesJsonApiCreation !== true) {\n body = JsonApiDataFactory.create(params.classKey, body);\n }\n\n const apiResponse = await serverRequest({\n method: \"PATCH\",\n url: buildServerUrl(params.endpoint),\n token,\n body,\n files: params.files,\n companyId: params.companyId,\n language: params.language,\n additionalHeaders: _serverConfig?.additionalHeaders,\n });\n\n return translateResponse({\n classKey: params.responseType ?? params.classKey,\n apiResponse,\n companyId: params.companyId,\n language: params.language,\n });\n}\n\nexport async function ServerJsonApiDelete(params: {\n classKey: ApiRequestDataTypeInterface;\n endpoint: string;\n companyId?: string;\n language: string;\n responseType?: ApiRequestDataTypeInterface;\n}): Promise<ApiResponseInterface> {\n runServerBootstrapper();\n const token = await getServerToken();\n\n const apiResponse = await serverRequest({\n method: \"DELETE\",\n url: buildServerUrl(params.endpoint),\n token,\n companyId: params.companyId,\n language: params.language,\n additionalHeaders: _serverConfig?.additionalHeaders,\n });\n\n return translateResponse({\n classKey: params.responseType ?? params.classKey,\n apiResponse,\n companyId: params.companyId,\n language: params.language,\n });\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
@@ -98,4 +98,4 @@ type ModuleFactory = (params: {
98
98
  }>;
99
99
  }) => ModuleWithPermissions;
100
100
 
101
- export { Action as A, type ModuleWithPermissions as M, type PageUrl as P, type ModuleFactory as a, type PermissionCheck as b, type ModulePermissionDefinition as c, type ModuleDefinition as d, type PermissionConfig as e, type PermissionModule as f, type PermissionUser as g };
101
+ export { Action as A, type ModuleFactory as M, type PageUrl as P, type ModuleWithPermissions as a, type PermissionCheck as b, type ModulePermissionDefinition as c, type ModuleDefinition as d, type PermissionConfig as e, type PermissionModule as f, type PermissionUser as g };
@@ -98,4 +98,4 @@ type ModuleFactory = (params: {
98
98
  }>;
99
99
  }) => ModuleWithPermissions;
100
100
 
101
- export { Action as A, type ModuleWithPermissions as M, type PageUrl as P, type ModuleFactory as a, type PermissionCheck as b, type ModulePermissionDefinition as c, type ModuleDefinition as d, type PermissionConfig as e, type PermissionModule as f, type PermissionUser as g };
101
+ export { Action as A, type ModuleFactory as M, type PageUrl as P, type ModuleWithPermissions as a, type PermissionCheck as b, type ModulePermissionDefinition as c, type ModuleDefinition as d, type PermissionConfig as e, type PermissionModule as f, type PermissionUser as g };
@@ -1,4 +1,4 @@
1
- import { P as PageUrl } from './types-bsm8Fp55.js';
1
+ import { P as PageUrl } from './types-Cn153-mH.js';
2
2
 
3
3
  declare function usePageUrlGenerator(): (params: {
4
4
  page?: PageUrl | string;
@@ -1,4 +1,4 @@
1
- import { P as PageUrl } from './types-Bq_UA8Lg.mjs';
1
+ import { P as PageUrl } from './types-D0uLYV2P.mjs';
2
2
 
3
3
  declare function usePageUrlGenerator(): (params: {
4
4
  page?: PageUrl | string;
@@ -1,4 +1,4 @@
1
- import { N as NotificationInterface } from './notification.interface-MnRt1Tjm.mjs';
1
+ import { N as NotificationInterface } from './notification.interface-P_pNfzZQ.mjs';
2
2
 
3
3
  interface UseSocketOptions {
4
4
  token: string;
@@ -1,4 +1,4 @@
1
- import { N as NotificationInterface } from './notification.interface-CjzV_KII.js';
1
+ import { N as NotificationInterface } from './notification.interface-BNCvnVCW.js';
2
2
 
3
3
  interface UseSocketOptions {
4
4
  token: string;
@@ -97,34 +97,4 @@ interface UserInterface extends ApiDataInterface {
97
97
  get modules(): ModuleInterface[];
98
98
  }
99
99
 
100
- type ContentInput = {
101
- id: string;
102
- name?: string;
103
- authorId?: string;
104
- editorIds?: string[];
105
- };
106
- interface ContentInterface extends ApiDataInterface {
107
- get contentType(): string | undefined;
108
- get name(): string;
109
- get abstract(): string | undefined;
110
- get tldr(): string | undefined;
111
- get aiStatus(): string;
112
- get relevance(): number | undefined;
113
- get author(): UserInterface;
114
- get editors(): UserInterface[];
115
- }
116
-
117
- type NotificationInput = {
118
- id: string;
119
- isRead: boolean;
120
- };
121
- interface NotificationInterface extends ApiDataInterface {
122
- get notificationType(): string;
123
- get isRead(): boolean;
124
- get message(): string | undefined;
125
- get actionUrl(): string | undefined;
126
- get actor(): ApiDataInterface | undefined;
127
- get subject(): ApiDataInterface | undefined;
128
- }
129
-
130
- export type { ContentInterface as C, NotificationInterface as N, RoleInput as R, UserInterface as U, ContentInput as a, CompanyInput as b, CompanyInterface as c, NotificationInput as d, RoleInterface as e, UserInput as f };
100
+ export type { CompanyInput as C, RoleInput as R, UserInterface as U, CompanyInterface as a, RoleInterface as b, UserInput as c };
@@ -97,34 +97,4 @@ interface UserInterface extends ApiDataInterface {
97
97
  get modules(): ModuleInterface[];
98
98
  }
99
99
 
100
- type ContentInput = {
101
- id: string;
102
- name?: string;
103
- authorId?: string;
104
- editorIds?: string[];
105
- };
106
- interface ContentInterface extends ApiDataInterface {
107
- get contentType(): string | undefined;
108
- get name(): string;
109
- get abstract(): string | undefined;
110
- get tldr(): string | undefined;
111
- get aiStatus(): string;
112
- get relevance(): number | undefined;
113
- get author(): UserInterface;
114
- get editors(): UserInterface[];
115
- }
116
-
117
- type NotificationInput = {
118
- id: string;
119
- isRead: boolean;
120
- };
121
- interface NotificationInterface extends ApiDataInterface {
122
- get notificationType(): string;
123
- get isRead(): boolean;
124
- get message(): string | undefined;
125
- get actionUrl(): string | undefined;
126
- get actor(): ApiDataInterface | undefined;
127
- get subject(): ApiDataInterface | undefined;
128
- }
129
-
130
- export type { ContentInterface as C, NotificationInterface as N, RoleInput as R, UserInterface as U, ContentInput as a, CompanyInput as b, CompanyInterface as c, NotificationInput as d, RoleInterface as e, UserInput as f };
100
+ export type { CompanyInput as C, RoleInput as R, UserInterface as U, CompanyInterface as a, RoleInterface as b, UserInput as c };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@carlonicora/nextjs-jsonapi",
3
- "version": "3.24.2",
3
+ "version": "3.25.0",
4
4
  "description": "Next.js JSON:API client with server/client support and caching",
5
5
  "author": "Carlo Nicora",
6
6
  "license": "GPL-3.0-or-later",
@@ -67,6 +67,16 @@
67
67
  "types": "./dist/features/tokenusage/index.d.ts",
68
68
  "import": "./dist/features/tokenusage/index.mjs",
69
69
  "require": "./dist/features/tokenusage/index.js"
70
+ },
71
+ "./analytics": {
72
+ "types": "./dist/features/analytics/index.d.ts",
73
+ "import": "./dist/features/analytics/index.mjs",
74
+ "require": "./dist/features/analytics/index.js"
75
+ },
76
+ "./analytics/server": {
77
+ "types": "./dist/features/analytics/server.d.ts",
78
+ "import": "./dist/features/analytics/server.mjs",
79
+ "require": "./dist/features/analytics/server.js"
70
80
  }
71
81
  },
72
82
  "scripts": {
@@ -21,6 +21,7 @@ import { useRoleTableStructure } from "../features/role/hooks";
21
21
  import { useUserTableStructure } from "../features/user/hooks";
22
22
  import { useStripePriceTableStructure } from "../features/billing/stripe-price/hooks/useStripePriceTableStructure";
23
23
  import { useStripeProductTableStructure } from "../features/billing/stripe-product/hooks/useStripeProductTableStructure";
24
+ import { useWaitlistTableStructure } from "../features/waitlist/hooks/useWaitlistTableStructure";
24
25
  import { registerTableGenerator } from "../hooks";
25
26
 
26
27
  export * from "../features/content/hooks";
@@ -34,4 +35,5 @@ registerTableGenerator("users", useUserTableStructure);
34
35
  registerTableGenerator("companies", useCompanyTableStructure);
35
36
  registerTableGenerator("stripe-products", useStripeProductTableStructure);
36
37
  registerTableGenerator("stripe-prices", useStripePriceTableStructure);
38
+ registerTableGenerator("waitlists", useWaitlistTableStructure);
37
39
  // Note: Content registration moved to app-level to support app-specific cellTopic
package/src/core/index.ts CHANGED
@@ -113,6 +113,10 @@ export * from "../features/tokenusage/tokenusage-admin.module";
113
113
  // rationale as the line above: a bootstrapper needs it, and it must not drag
114
114
  // the feature's recharts client bundle in.
115
115
  export * from "../features/tokenusage/tokenusage.modules";
116
+ // The analytics module bundle. Same placement rationale as the tokenusage lines
117
+ // above: a bootstrapper needs it, and it must not drag the feature's client
118
+ // bundle in.
119
+ export * from "../features/analytics/analytics.modules";
116
120
  // configureTokenUsage() is called from the app's bootstrap file, which is
117
121
  // evaluated on the server as well as the client. Reaching it through the
118
122
  // client-marked ./tokenusage barrel makes it a client function, and calling one
@@ -73,6 +73,13 @@ export interface FoundationModuleDefinitions {
73
73
  TokenUsageReportSummary: ModuleWithPermissions;
74
74
  TokenUsageReportTimeline: ModuleWithPermissions;
75
75
  TokenUsageReportBreakdown: ModuleWithPermissions;
76
+ // First-party web analytics modules (tracker + administrative dashboard)
77
+ AnalyticsEvent: ModuleWithPermissions;
78
+ AnalyticsSummary: ModuleWithPermissions;
79
+ AnalyticsTimeline: ModuleWithPermissions;
80
+ AnalyticsBreakdown: ModuleWithPermissions;
81
+ AnalyticsSession: ModuleWithPermissions;
82
+ AnalyticsPageView: ModuleWithPermissions;
76
83
  }
77
84
 
78
85
  // App-specific modules - apps will augment this interface ONLY
@@ -34,6 +34,7 @@ const DECLARED_FOR_APP_RENDERED_SURFACES = new Set<string>([
34
34
  "administration.token_usage.description",
35
35
  "administration.users.description",
36
36
  "administration.ai_connections.description",
37
+ "analytics.admin.description",
37
38
  "handbook.chat.delete_confirm",
38
39
  "handbook.chat.rename_placeholder",
39
40
  "handbook.contents.title",
@@ -0,0 +1,64 @@
1
+ import { beforeEach, describe, expect, it, vi } from "vitest";
2
+ import { AnalyticsAdminPageContainer, AnalyticsJourneyPageContainer } from "../index";
3
+ import { AnalyticsAdminPage, AnalyticsJourneyPage } from "../server-entry";
4
+
5
+ const mocks = vi.hoisted(() => ({
6
+ hasRole: vi.fn(),
7
+ redirect: vi.fn((url: string) => {
8
+ // next/navigation's redirect throws to stop rendering; mirror that.
9
+ throw new Error(`NEXT_REDIRECT:${url}`);
10
+ }),
11
+ }));
12
+
13
+ // ServerSession reads next/headers cookies, which only exist inside a request.
14
+ vi.mock("../../../server", () => ({
15
+ ServerSession: { hasRole: mocks.hasRole },
16
+ }));
17
+
18
+ vi.mock("next/navigation", () => ({
19
+ redirect: mocks.redirect,
20
+ }));
21
+
22
+ describe("analytics server entry", () => {
23
+ beforeEach(() => {
24
+ vi.clearAllMocks();
25
+ });
26
+
27
+ it("AnalyticsAdminPage renders the admin container for the role holder", async () => {
28
+ mocks.hasRole.mockResolvedValue(true);
29
+
30
+ const element = await AnalyticsAdminPage({
31
+ adminRoleId: "role-admin",
32
+ pageUrl: "/admin/stats",
33
+ sessionPageUrl: "/admin/stats/sessions",
34
+ });
35
+
36
+ expect(mocks.hasRole).toHaveBeenCalledWith("role-admin");
37
+ expect(element.type).toBe(AnalyticsAdminPageContainer);
38
+ expect(element.props).toEqual({ pageUrl: "/admin/stats", sessionPageUrl: "/admin/stats/sessions" });
39
+ });
40
+
41
+ it("AnalyticsAdminPage redirects to `/` by default without the role", async () => {
42
+ mocks.hasRole.mockResolvedValue(false);
43
+
44
+ await expect(AnalyticsAdminPage({ adminRoleId: "role-admin" })).rejects.toThrow("NEXT_REDIRECT:/");
45
+ expect(mocks.redirect).toHaveBeenCalledWith("/");
46
+ });
47
+
48
+ it("AnalyticsJourneyPage renders the journey container for the role holder", async () => {
49
+ mocks.hasRole.mockResolvedValue(true);
50
+
51
+ const element = await AnalyticsJourneyPage({ adminRoleId: "role-admin", sessionId: "s1", pageUrl: "/admin/stats" });
52
+
53
+ expect(element.type).toBe(AnalyticsJourneyPageContainer);
54
+ expect(element.props).toEqual({ sessionId: "s1", pageUrl: "/admin/stats" });
55
+ });
56
+
57
+ it("AnalyticsJourneyPage redirects to deniedRedirect without the role", async () => {
58
+ mocks.hasRole.mockResolvedValue(false);
59
+
60
+ await expect(
61
+ AnalyticsJourneyPage({ adminRoleId: "role-admin", sessionId: "s1", deniedRedirect: "/401" }),
62
+ ).rejects.toThrow("NEXT_REDIRECT:/401");
63
+ });
64
+ });
@@ -0,0 +1,30 @@
1
+ import { ModuleFactory, ModuleWithPermissions } from "../../permissions";
2
+ import { AnalyticsBreakdown } from "./data/analytics-breakdown";
3
+ import { AnalyticsEvent } from "./data/analytics-event";
4
+ import { AnalyticsPageView } from "./data/analytics-page-view";
5
+ import { AnalyticsSession } from "./data/analytics-session";
6
+ import { AnalyticsSummary } from "./data/analytics-summary";
7
+ import { AnalyticsTimeline } from "./data/analytics-timeline";
8
+
9
+ /**
10
+ * Every resource behind the analytics tracker and dashboard, as one object a
11
+ * consuming app spreads into its `allModules`. Same rationale as
12
+ * `tokenUsageModules`: one spread cannot be partially done, and a forgotten
13
+ * name is `undefined` at runtime even though `Modules.X` typechecks.
14
+ *
15
+ * `AnalyticsPageView` is only a child segment: the service builds
16
+ * `analytics/administration/sessions/{id}/pageviews` with EndpointCreator's
17
+ * `childEndpoint`. None has a pageUrl — these are not navigable resources.
18
+ *
19
+ * `satisfies` keeps the constraint without widening the return type to an
20
+ * index signature (see tokenusage.modules.ts).
21
+ */
22
+ export const analyticsModules = (factory: ModuleFactory) =>
23
+ ({
24
+ AnalyticsEvent: factory({ name: "analytics/events", model: AnalyticsEvent }),
25
+ AnalyticsSummary: factory({ name: "analytics/administration/summary", model: AnalyticsSummary }),
26
+ AnalyticsTimeline: factory({ name: "analytics/administration/timeline", model: AnalyticsTimeline }),
27
+ AnalyticsBreakdown: factory({ name: "analytics/administration/breakdown", model: AnalyticsBreakdown }),
28
+ AnalyticsSession: factory({ name: "analytics/administration/sessions", model: AnalyticsSession }),
29
+ AnalyticsPageView: factory({ name: "pageviews", model: AnalyticsPageView }),
30
+ }) satisfies Record<string, ModuleWithPermissions>;
@@ -0,0 +1,97 @@
1
+ "use client";
2
+
3
+ import { useTranslations } from "next-intl";
4
+ import { RoundPageContainer } from "../../../components";
5
+ import { Card, CardContent, CardHeader, CardTitle } from "../../../shadcnui";
6
+ import { useAnalyticsAdmin } from "../contexts/AnalyticsAdminContext";
7
+ import { AnalyticsAdminTiles } from "./AnalyticsAdminTiles";
8
+ import { AnalyticsBreakdownTable } from "./AnalyticsBreakdownTable";
9
+ import { AnalyticsSessionsTable } from "./AnalyticsSessionsTable";
10
+ import { AnalyticsTimelineChart } from "./AnalyticsTimelineChart";
11
+
12
+ /**
13
+ * Page body for the administrative analytics dashboard.
14
+ *
15
+ * Stateless by design — every value it renders comes from
16
+ * `useAnalyticsAdmin()`. The filter bar is deliberately NOT here: it belongs to
17
+ * the page title bar, which `RoundPageContainer` fills from `SharedContext`, so
18
+ * the provider publishes it (see AnalyticsAdminContext).
19
+ */
20
+ export function AnalyticsAdminContainer() {
21
+ const t = useTranslations();
22
+ const {
23
+ summary,
24
+ timeline,
25
+ breakdowns,
26
+ sessions,
27
+ loadMoreSessions,
28
+ hasMoreSessions,
29
+ sessionPageUrl,
30
+ isLoading,
31
+ error,
32
+ } = useAnalyticsAdmin();
33
+
34
+ if (error) {
35
+ return (
36
+ <RoundPageContainer fullWidth forceHeader>
37
+ <div className="p-4">
38
+ <Card>
39
+ <CardContent>
40
+ <p className="text-destructive text-xs/relaxed">{error}</p>
41
+ </CardContent>
42
+ </Card>
43
+ </div>
44
+ </RoundPageContainer>
45
+ );
46
+ }
47
+
48
+ // Loading renders nothing in the body: the title bar (with the filter bar) is
49
+ // already mounted, so a spinner would only make the controls jump on arrival.
50
+ if (isLoading) return <RoundPageContainer fullWidth forceHeader />;
51
+
52
+ return (
53
+ <RoundPageContainer fullWidth forceHeader>
54
+ <div className="flex w-full flex-col gap-4 p-4">
55
+ <div className="flex flex-col gap-2">
56
+ <AnalyticsAdminTiles summary={summary} />
57
+ {/* Days past the raw-event retention are served from daily summaries.
58
+ Whether the window reaches them is the backend's knowledge, not the
59
+ client's, so the caption is always shown. */}
60
+ <p className="text-muted-foreground text-xs">{t("analytics.admin.tiles.summarised_note")}</p>
61
+ </div>
62
+
63
+ <Card>
64
+ <CardHeader>
65
+ <CardTitle>{t("analytics.admin.panels.over_time")}</CardTitle>
66
+ </CardHeader>
67
+ <CardContent>
68
+ <AnalyticsTimelineChart rows={timeline} />
69
+ </CardContent>
70
+ </Card>
71
+
72
+ <Card>
73
+ <CardHeader>
74
+ <CardTitle>{t("analytics.admin.panels.breakdown")}</CardTitle>
75
+ </CardHeader>
76
+ <CardContent>
77
+ <AnalyticsBreakdownTable breakdowns={breakdowns} />
78
+ </CardContent>
79
+ </Card>
80
+
81
+ <Card>
82
+ <CardHeader>
83
+ <CardTitle>{t("analytics.admin.panels.sessions")}</CardTitle>
84
+ </CardHeader>
85
+ <CardContent>
86
+ <AnalyticsSessionsTable
87
+ rows={sessions}
88
+ sessionPageUrl={sessionPageUrl}
89
+ onLoadMore={loadMoreSessions}
90
+ hasMore={hasMoreSessions}
91
+ />
92
+ </CardContent>
93
+ </Card>
94
+ </div>
95
+ </RoundPageContainer>
96
+ );
97
+ }
@@ -0,0 +1,117 @@
1
+ "use client";
2
+
3
+ import { useTranslations } from "next-intl";
4
+ import { DateRangeSelector } from "../../../components/forms/DateRangeSelector";
5
+ import { Button } from "../../../shadcnui";
6
+ import { cn } from "../../../utils";
7
+ import {
8
+ ANALYTICS_GRANULARITIES,
9
+ ANALYTICS_SECTIONS,
10
+ type AnalyticsGranularity,
11
+ type AnalyticsSection,
12
+ } from "../data/analytics.types";
13
+
14
+ type FilterState = {
15
+ from: string;
16
+ to: string;
17
+ section: AnalyticsSection;
18
+ granularity: AnalyticsGranularity;
19
+ };
20
+
21
+ type Props = FilterState & {
22
+ /** Receives ONLY the keys that changed. */
23
+ onChange: (next: Partial<FilterState>) => void;
24
+ };
25
+
26
+ /**
27
+ * The single control row above the KPI tiles.
28
+ *
29
+ * It is deliberately stateless: every control reports the one key it changed and
30
+ * the owning context re-fetches. Keeping the whole filter state in one place is
31
+ * what lets the page issue a single coordinated batch of requests instead of one
32
+ * per control.
33
+ */
34
+ export function AnalyticsAdminFilterBar({ section, granularity, onChange }: Props) {
35
+ const t = useTranslations();
36
+
37
+ const sectionLabels: Record<AnalyticsSection, string> = {
38
+ all: t("analytics.admin.filter.section_all"),
39
+ public: t("analytics.admin.filter.section_public"),
40
+ app: t("analytics.admin.filter.section_app"),
41
+ };
42
+
43
+ const granularityLabels: Record<AnalyticsGranularity, string> = {
44
+ day: t("analytics.admin.filter.granularity_day"),
45
+ week: t("analytics.admin.filter.granularity_week"),
46
+ month: t("analytics.admin.filter.granularity_month"),
47
+ };
48
+
49
+ return (
50
+ <div className="flex flex-wrap items-center gap-2">
51
+ <DateRangeSelector
52
+ onDateChange={(range) => {
53
+ if (!range?.from || !range?.to) return;
54
+ onChange({ from: range.from.toISOString(), to: range.to.toISOString() });
55
+ }}
56
+ />
57
+
58
+ <Segmented
59
+ ariaLabel={t("analytics.admin.filter.section")}
60
+ value={section}
61
+ options={ANALYTICS_SECTIONS.map((value) => ({ value, label: sectionLabels[value] }))}
62
+ onSelect={(value) => onChange({ section: value })}
63
+ />
64
+
65
+ <Segmented
66
+ ariaLabel={t("analytics.admin.filter.granularity")}
67
+ value={granularity}
68
+ options={ANALYTICS_GRANULARITIES.map((value) => ({ value, label: granularityLabels[value] }))}
69
+ onSelect={(value) => onChange({ granularity: value })}
70
+ />
71
+ </div>
72
+ );
73
+ }
74
+
75
+ /**
76
+ * A segmented control built from plain Buttons.
77
+ *
78
+ * The design system has no ToggleGroup primitive, and Base UI triggers may never
79
+ * wrap a Button, so the segmented look is composed from Buttons directly — the
80
+ * selected segment takes the solid variant, the rest stay ghosts.
81
+ */
82
+ function Segmented<T extends string>({
83
+ ariaLabel,
84
+ value,
85
+ options,
86
+ onSelect,
87
+ }: {
88
+ ariaLabel: string;
89
+ value: T;
90
+ options: { value: T; label: string }[];
91
+ onSelect: (value: T) => void;
92
+ }) {
93
+ return (
94
+ <div
95
+ role="group"
96
+ aria-label={ariaLabel}
97
+ className="border-border inline-flex items-center gap-0.5 rounded-md border p-0.5"
98
+ >
99
+ {options.map((option) => {
100
+ const selected = option.value === value;
101
+ return (
102
+ <Button
103
+ key={option.value}
104
+ type="button"
105
+ size="sm"
106
+ variant={selected ? "default" : "ghost"}
107
+ aria-pressed={selected}
108
+ className={cn(!selected && "text-muted-foreground")}
109
+ onClick={() => onSelect(option.value)}
110
+ >
111
+ {option.label}
112
+ </Button>
113
+ );
114
+ })}
115
+ </div>
116
+ );
117
+ }
@@ -0,0 +1,25 @@
1
+ "use client";
2
+
3
+ import { AnalyticsAdminProvider } from "../contexts/AnalyticsAdminContext";
4
+ import { AnalyticsAdminContainer } from "./AnalyticsAdminContainer";
5
+
6
+ type Props = {
7
+ /** Route the breadcrumb links back to. Defaults to the provider's default. */
8
+ pageUrl?: string;
9
+ /** Base route of the journey page each session row links to. */
10
+ sessionPageUrl?: string;
11
+ };
12
+
13
+ /**
14
+ * The whole administrative analytics page as one client component, so a host
15
+ * app's server page renders a single element. `AnalyticsAdminContainer` owns
16
+ * the `RoundPageContainer`; the provider sits above it because it publishes the
17
+ * filter bar into the title bar.
18
+ */
19
+ export function AnalyticsAdminPageContainer({ pageUrl, sessionPageUrl }: Props) {
20
+ return (
21
+ <AnalyticsAdminProvider pageUrl={pageUrl} sessionPageUrl={sessionPageUrl}>
22
+ <AnalyticsAdminContainer />
23
+ </AnalyticsAdminProvider>
24
+ );
25
+ }