@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.
- package/dist/{AssistantMessageInterface-1Kbz8RVs.d.ts → AssistantMessageInterface-CubxuBIJ.d.ts} +1 -1
- package/dist/{AssistantMessageInterface-CoxJyuFM.d.mts → AssistantMessageInterface-sHjI2zPN.d.mts} +1 -1
- package/dist/{BlockNoteEditor-65HR6ULV.mjs → BlockNoteEditor-46O3JC4Z.mjs} +5 -5
- package/dist/{BlockNoteEditor-35D4R7HJ.js → BlockNoteEditor-HKS6AXDA.js} +20 -20
- package/dist/{BlockNoteEditor-35D4R7HJ.js.map → BlockNoteEditor-HKS6AXDA.js.map} +1 -1
- package/dist/{BlockNoteViewer-QDXDVYEM.js → BlockNoteViewer-7BFNTN4E.js} +4 -4
- package/dist/{BlockNoteViewer-QDXDVYEM.js.map → BlockNoteViewer-7BFNTN4E.js.map} +1 -1
- package/dist/{BlockNoteViewer-KDKGUIG5.mjs → BlockNoteViewer-KRTAFTUX.mjs} +3 -3
- package/dist/{HandbookSectionInterface-OePKPbqN.d.ts → HandbookSectionInterface-Ck1wZJ81.d.ts} +1 -1
- package/dist/{HandbookSectionInterface-BYsmnPEz.d.mts → HandbookSectionInterface-DgYH--Ik.d.mts} +1 -1
- package/dist/JsonApiRequest-TE5WD3WS.js +26 -0
- package/dist/{JsonApiRequest-G625QAH5.js.map → JsonApiRequest-TE5WD3WS.js.map} +1 -1
- package/dist/{JsonApiRequest-RRMNV7YE.mjs → JsonApiRequest-UHOWNI3O.mjs} +2 -2
- package/dist/analytics.modules-CfczU12P.d.ts +25 -0
- package/dist/analytics.modules-pzGwMASI.d.mts +25 -0
- package/dist/{auth.interface-BC1jp_ni.d.ts → auth.interface-BzI4onI_.d.ts} +1 -1
- package/dist/{auth.interface-dR2-6gPM.d.mts → auth.interface-Df4vXbNz.d.mts} +1 -1
- package/dist/billing/index.js +402 -402
- package/dist/billing/index.mjs +4 -4
- package/dist/{chunk-YXRVPEEJ.js → chunk-26MQIJNB.js} +1 -1
- package/dist/{chunk-YXRVPEEJ.js.map → chunk-26MQIJNB.js.map} +1 -1
- package/dist/{chunk-T76WLDE5.mjs → chunk-37DMFVLN.mjs} +1 -1
- package/dist/chunk-4Z2UONN2.mjs +110 -0
- package/dist/chunk-4Z2UONN2.mjs.map +1 -0
- package/dist/{chunk-3AR4KA5D.js → chunk-4Z45NYPR.js} +13 -13
- package/dist/{chunk-3AR4KA5D.js.map → chunk-4Z45NYPR.js.map} +1 -1
- package/dist/chunk-7B4E42R5.mjs +280 -0
- package/dist/chunk-7B4E42R5.mjs.map +1 -0
- package/dist/{chunk-U7QVISQS.js → chunk-CAKOFJZD.js} +450 -40
- package/dist/chunk-CAKOFJZD.js.map +1 -0
- package/dist/{chunk-IBVKPLHT.mjs → chunk-E3I4MT53.mjs} +422 -12
- package/dist/chunk-E3I4MT53.mjs.map +1 -0
- package/dist/chunk-GHFMKH3M.js +280 -0
- package/dist/chunk-GHFMKH3M.js.map +1 -0
- package/dist/chunk-KLV6RLDE.js +110 -0
- package/dist/chunk-KLV6RLDE.js.map +1 -0
- package/dist/{chunk-ZJI5ZZW5.mjs → chunk-Q6J2ICKO.mjs} +3 -3
- package/dist/{chunk-4O7R2Z2O.mjs → chunk-XDYILJ2S.mjs} +1749 -1737
- package/dist/{chunk-4O7R2Z2O.mjs.map → chunk-XDYILJ2S.mjs.map} +1 -1
- package/dist/{chunk-2XCUAHQT.js → chunk-Y3OKEQGL.js} +1126 -1114
- package/dist/chunk-Y3OKEQGL.js.map +1 -0
- package/dist/client/index.d.mts +6 -5
- package/dist/client/index.d.ts +6 -5
- package/dist/client/index.js +5 -5
- package/dist/client/index.js.map +1 -1
- package/dist/client/index.mjs +4 -4
- package/dist/components/index.d.mts +17 -7
- package/dist/components/index.d.ts +17 -7
- package/dist/components/index.js +5 -5
- package/dist/components/index.mjs +4 -4
- package/dist/{config-B6UB7j69.d.ts → config-3N1LSrcC.d.ts} +1 -1
- package/dist/{config-Cd5xTEfZ.d.ts → config-AVtuvfM1.d.ts} +1 -1
- package/dist/{config-n-ZpPmOL.d.mts → config-BSKTYDoh.d.mts} +1 -1
- package/dist/{config-YyhFHxkU.d.mts → config-BmufyEj6.d.mts} +1 -1
- package/dist/contexts/index.d.mts +5 -4
- package/dist/contexts/index.d.ts +5 -4
- package/dist/contexts/index.js +5 -5
- package/dist/contexts/index.mjs +4 -4
- package/dist/core/index.d.mts +31 -14
- package/dist/core/index.d.ts +31 -14
- package/dist/core/index.js +9 -5
- package/dist/core/index.js.map +1 -1
- package/dist/core/index.mjs +8 -4
- package/dist/features/analytics/index.css +30 -0
- package/dist/features/analytics/index.css.map +1 -0
- package/dist/features/analytics/index.d.mts +508 -0
- package/dist/features/analytics/index.d.ts +508 -0
- package/dist/features/analytics/index.js +1166 -0
- package/dist/features/analytics/index.js.map +1 -0
- package/dist/features/analytics/index.mjs +1166 -0
- package/dist/features/analytics/index.mjs.map +1 -0
- package/dist/features/analytics/server.d.mts +28 -0
- package/dist/features/analytics/server.d.ts +28 -0
- package/dist/features/analytics/server.js +41 -0
- package/dist/features/analytics/server.js.map +1 -0
- package/dist/features/analytics/server.mjs +41 -0
- package/dist/features/analytics/server.mjs.map +1 -0
- package/dist/features/help/index.d.mts +2 -2
- package/dist/features/help/index.d.ts +2 -2
- package/dist/features/help/index.js +38 -38
- package/dist/features/help/index.mjs +4 -4
- package/dist/features/tokenusage/index.d.mts +2 -2
- package/dist/features/tokenusage/index.d.ts +2 -2
- package/dist/features/tokenusage/index.js +139 -221
- package/dist/features/tokenusage/index.js.map +1 -1
- package/dist/features/tokenusage/index.mjs +26 -108
- package/dist/features/tokenusage/index.mjs.map +1 -1
- package/dist/index.d.mts +11 -9
- package/dist/index.d.ts +11 -9
- package/dist/index.js +10 -6
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +9 -5
- package/dist/notification.interface-BNCvnVCW.d.ts +34 -0
- package/dist/notification.interface-P_pNfzZQ.d.mts +34 -0
- package/dist/{s3.service-Bqr-fQmi.d.ts → s3.service-CbUa6a-8.d.ts} +3 -2
- package/dist/{s3.service-Buiy1ncj.d.mts → s3.service-DAp8FbuG.d.mts} +3 -2
- package/dist/server/index.d.mts +5 -4
- package/dist/server/index.d.ts +5 -4
- package/dist/server/index.js +39 -279
- package/dist/server/index.js.map +1 -1
- package/dist/server/index.mjs +20 -260
- package/dist/server/index.mjs.map +1 -1
- package/dist/{types-bsm8Fp55.d.ts → types-Cn153-mH.d.ts} +1 -1
- package/dist/{types-Bq_UA8Lg.d.mts → types-D0uLYV2P.d.mts} +1 -1
- package/dist/{usePageUrlGenerator-J_ECv-oQ.d.ts → usePageUrlGenerator-B-3FRKBD.d.ts} +1 -1
- package/dist/{usePageUrlGenerator-uKBjAKqC.d.mts → usePageUrlGenerator-CCJ_cdQy.d.mts} +1 -1
- package/dist/{useSocket-BcH7l7B8.d.mts → useSocket-B9gTbuCj.d.mts} +1 -1
- package/dist/{useSocket-DCZd_eJk.d.ts → useSocket-BHVIyigI.d.ts} +1 -1
- package/dist/{notification.interface-MnRt1Tjm.d.mts → user.interface-CSaGWFbg.d.mts} +1 -31
- package/dist/{notification.interface-CjzV_KII.d.ts → user.interface-CrmwJLYW.d.ts} +1 -31
- package/package.json +11 -1
- package/src/client/index.ts +2 -0
- package/src/core/index.ts +4 -0
- package/src/core/registry/ModuleRegistry.ts +7 -0
- package/src/features/__tests__/i18n-keys.contract.spec.ts +1 -0
- package/src/features/analytics/__tests__/server-entry.spec.tsx +64 -0
- package/src/features/analytics/analytics.modules.ts +30 -0
- package/src/features/analytics/components/AnalyticsAdminContainer.tsx +97 -0
- package/src/features/analytics/components/AnalyticsAdminFilterBar.tsx +117 -0
- package/src/features/analytics/components/AnalyticsAdminPageContainer.tsx +25 -0
- package/src/features/analytics/components/AnalyticsAdminTiles.tsx +191 -0
- package/src/features/analytics/components/AnalyticsBreakdownTable.tsx +104 -0
- package/src/features/analytics/components/AnalyticsJourneyContainer.tsx +246 -0
- package/src/features/analytics/components/AnalyticsJourneyPageContainer.tsx +14 -0
- package/src/features/analytics/components/AnalyticsSessionsTable.tsx +107 -0
- package/src/features/analytics/components/AnalyticsTimelineChart.tsx +175 -0
- package/src/features/analytics/components/AnalyticsTracker.tsx +112 -0
- package/src/features/analytics/components/__tests__/AnalyticsAdminTiles.spec.tsx +39 -0
- package/src/features/analytics/components/__tests__/AnalyticsBreakdownTable.spec.tsx +39 -0
- package/src/features/analytics/components/__tests__/AnalyticsJourneyContainer.spec.tsx +124 -0
- package/src/features/analytics/components/__tests__/AnalyticsPageContainers.spec.tsx +117 -0
- package/src/features/analytics/components/__tests__/AnalyticsSessionsTable.spec.tsx +121 -0
- package/src/features/analytics/components/__tests__/AnalyticsTimelineChart.spec.tsx +35 -0
- package/src/features/analytics/components/__tests__/AnalyticsTracker.spec.tsx +248 -0
- package/src/features/analytics/components/server/AnalyticsAdminPage.tsx +28 -0
- package/src/features/analytics/components/server/AnalyticsJourneyPage.tsx +25 -0
- package/src/features/analytics/contexts/AnalyticsAdminContext.tsx +273 -0
- package/src/features/analytics/contexts/__tests__/AnalyticsAdminContext.spec.tsx +140 -0
- package/src/features/analytics/data/AnalyticsAdminService.ts +91 -0
- package/src/features/analytics/data/AnalyticsService.ts +23 -0
- package/src/features/analytics/data/__tests__/AnalyticsAdminService.spec.ts +117 -0
- package/src/features/analytics/data/__tests__/AnalyticsService.spec.ts +56 -0
- package/src/features/analytics/data/__tests__/models.spec.ts +300 -0
- package/src/features/analytics/data/analytics-breakdown.interface.ts +9 -0
- package/src/features/analytics/data/analytics-breakdown.ts +65 -0
- package/src/features/analytics/data/analytics-event.interface.ts +7 -0
- package/src/features/analytics/data/analytics-event.ts +63 -0
- package/src/features/analytics/data/analytics-page-view.interface.ts +9 -0
- package/src/features/analytics/data/analytics-page-view.ts +57 -0
- package/src/features/analytics/data/analytics-session.interface.ts +23 -0
- package/src/features/analytics/data/analytics-session.ts +144 -0
- package/src/features/analytics/data/analytics-summary.interface.ts +11 -0
- package/src/features/analytics/data/analytics-summary.ts +79 -0
- package/src/features/analytics/data/analytics-timeline.interface.ts +10 -0
- package/src/features/analytics/data/analytics-timeline.ts +67 -0
- package/src/features/analytics/data/analytics.types.ts +97 -0
- package/src/features/analytics/data/index.ts +15 -0
- package/src/features/analytics/i18n-keys.ts +76 -0
- package/src/features/analytics/index.ts +31 -0
- package/src/features/analytics/lib/__tests__/utm.spec.ts +20 -0
- package/src/features/analytics/lib/__tests__/visitorCookie.spec.ts +63 -0
- package/src/features/analytics/lib/utm.ts +29 -0
- package/src/features/analytics/lib/visitorCookie.ts +32 -0
- package/src/features/analytics/server-entry.ts +8 -0
- package/src/features/waitlist/components/lists/WaitlistList.tsx +68 -121
- package/src/features/waitlist/data/WaitlistService.ts +2 -2
- package/src/features/waitlist/data/index.ts +1 -0
- package/src/features/waitlist/data/waitlist.fields.ts +8 -0
- package/src/features/waitlist/hooks/useWaitlistTableStructure.tsx +69 -30
- package/dist/JsonApiRequest-G625QAH5.js +0 -26
- package/dist/chunk-2XCUAHQT.js.map +0 -1
- package/dist/chunk-IBVKPLHT.mjs.map +0 -1
- package/dist/chunk-U7QVISQS.js.map +0 -1
- /package/dist/{BlockNoteEditor-65HR6ULV.mjs.map → BlockNoteEditor-46O3JC4Z.mjs.map} +0 -0
- /package/dist/{BlockNoteViewer-KDKGUIG5.mjs.map → BlockNoteViewer-KRTAFTUX.mjs.map} +0 -0
- /package/dist/{JsonApiRequest-RRMNV7YE.mjs.map → JsonApiRequest-UHOWNI3O.mjs.map} +0 -0
- /package/dist/{chunk-T76WLDE5.mjs.map → chunk-37DMFVLN.mjs.map} +0 -0
- /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
|
|
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
|
|
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 };
|
|
@@ -97,34 +97,4 @@ interface UserInterface extends ApiDataInterface {
|
|
|
97
97
|
get modules(): ModuleInterface[];
|
|
98
98
|
}
|
|
99
99
|
|
|
100
|
-
type
|
|
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
|
|
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.
|
|
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": {
|
package/src/client/index.ts
CHANGED
|
@@ -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
|
+
}
|