@graph8/sdk 0.373.0 → 0.374.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 (279) hide show
  1. package/README.md +3 -3
  2. package/blocks/_foundation/README.md +47 -0
  3. package/blocks/_foundation/block.json +13 -0
  4. package/blocks/_foundation/components/auth-gate.tsx +363 -0
  5. package/blocks/_foundation/server/auth.ts +815 -0
  6. package/blocks/campaigns/README.md +1 -1
  7. package/blocks/campaigns/block.json +34 -1
  8. package/blocks/campaigns/components/campaign-links.tsx +180 -0
  9. package/blocks/company-intelligence/components/research-run.tsx +6 -2
  10. package/blocks/crm/README.md +19 -0
  11. package/blocks/crm/block.json +205 -6
  12. package/blocks/crm/components/bulk-update.tsx +119 -0
  13. package/blocks/crm/components/contact-documents-tab.tsx +144 -0
  14. package/blocks/crm/components/contact-insights-tab.tsx +118 -0
  15. package/blocks/crm/components/contact-lookup-link.tsx +47 -6
  16. package/blocks/crm/components/contact-row-actions.tsx +43 -0
  17. package/blocks/crm/components/contact-tag-chips.tsx +113 -0
  18. package/blocks/crm/components/contacts-screen.tsx +46 -5
  19. package/blocks/crm/components/owner-assign.tsx +132 -0
  20. package/blocks/crm/components/record-open-links.tsx +79 -0
  21. package/blocks/crm/components/tag-assign.tsx +112 -0
  22. package/blocks/crm/components/tag-manager.tsx +178 -0
  23. package/blocks/crm/components/unlock-info-button.tsx +82 -0
  24. package/blocks/crm/components/verify-email-button.tsx +93 -0
  25. package/blocks/crm/hooks/use-contact-tags.ts +29 -0
  26. package/blocks/crm/lib/record-actions.ts +102 -0
  27. package/blocks/deals/README.md +1 -0
  28. package/blocks/deals/block.json +17 -0
  29. package/blocks/deals/components/create-deal-button.tsx +109 -0
  30. package/blocks/deals/components/deal-detail.tsx +5 -3
  31. package/blocks/dialer/components/call-log-screen.tsx +6 -1
  32. package/blocks/documents/README.md +86 -0
  33. package/blocks/documents/block.json +378 -0
  34. package/blocks/documents/components/account-bar.tsx +48 -0
  35. package/blocks/documents/components/correspondence-form.tsx +95 -0
  36. package/blocks/documents/components/correspondence-import.tsx +106 -0
  37. package/blocks/documents/components/correspondence-panel.tsx +191 -0
  38. package/blocks/documents/components/documents-screen.tsx +76 -0
  39. package/blocks/documents/components/library-document-detail.tsx +175 -0
  40. package/blocks/documents/components/library-document-form.tsx +105 -0
  41. package/blocks/documents/components/library-folder-form.tsx +74 -0
  42. package/blocks/documents/components/library-panel.tsx +235 -0
  43. package/blocks/documents/components/library-upload.tsx +160 -0
  44. package/blocks/documents/components/question-row.tsx +130 -0
  45. package/blocks/documents/components/questionnaire-detail.tsx +197 -0
  46. package/blocks/documents/components/questionnaires-panel.tsx +173 -0
  47. package/blocks/documents/components/record-documents-tab.tsx +109 -0
  48. package/blocks/documents/lib/documents-types.ts +252 -0
  49. package/blocks/documents/lib/download.ts +64 -0
  50. package/blocks/documents/recipes/follow-a-questionnaire.ts +26 -0
  51. package/blocks/documents/recipes/log-account-correspondence.ts +27 -0
  52. package/blocks/documents/recipes/organize-the-library.ts +25 -0
  53. package/blocks/email-signatures/README.md +123 -0
  54. package/blocks/email-signatures/block.json +341 -0
  55. package/blocks/email-signatures/components/email-signatures-screen.tsx +166 -0
  56. package/blocks/email-signatures/components/signature-assign-template.tsx +119 -0
  57. package/blocks/email-signatures/components/signature-block-editor.tsx +87 -0
  58. package/blocks/email-signatures/components/signature-campaign-form.tsx +165 -0
  59. package/blocks/email-signatures/components/signature-campaigns.tsx +75 -0
  60. package/blocks/email-signatures/components/signature-identity-form.tsx +131 -0
  61. package/blocks/email-signatures/components/signature-mailboxes.tsx +195 -0
  62. package/blocks/email-signatures/components/signature-policy.tsx +130 -0
  63. package/blocks/email-signatures/components/signature-preview.tsx +119 -0
  64. package/blocks/email-signatures/components/signature-report.tsx +77 -0
  65. package/blocks/email-signatures/components/signature-template-details.tsx +112 -0
  66. package/blocks/email-signatures/components/signature-template-form.tsx +109 -0
  67. package/blocks/email-signatures/hooks/use-signatures-data.ts +93 -0
  68. package/blocks/email-signatures/lib/signatures-types.ts +392 -0
  69. package/blocks/email-signatures/recipes/assign-a-signature-to-a-new-hire.ts +32 -0
  70. package/blocks/email-signatures/recipes/create-a-template-and-make-it-default.ts +35 -0
  71. package/blocks/email-signatures/recipes/signature-engagement-report.ts +16 -0
  72. package/blocks/enrichment/README.md +22 -4
  73. package/blocks/enrichment/block.json +145 -5
  74. package/blocks/enrichment/components/auto-enrichment-configs.tsx +134 -0
  75. package/blocks/enrichment/components/auto-enrichment-settings.tsx +82 -0
  76. package/blocks/enrichment/components/bulk-enrich-panel.tsx +7 -7
  77. package/blocks/enrichment/components/enrichment-activity.tsx +93 -0
  78. package/blocks/enrichment/components/enrichment-job-detail.tsx +18 -0
  79. package/blocks/enrichment/components/enrichment-screen.tsx +34 -3
  80. package/blocks/enrichment/components/list-pipelines.tsx +97 -0
  81. package/blocks/enrichment/components/pipeline-detail.tsx +214 -0
  82. package/blocks/enrichment/components/pipeline-editor.tsx +144 -0
  83. package/blocks/enrichment/components/pipeline-templates.tsx +90 -0
  84. package/blocks/enrichment/lib/enrichment-types.ts +12 -0
  85. package/blocks/enrichment/lib/pipeline-types.ts +234 -0
  86. package/blocks/forms/README.md +2 -0
  87. package/blocks/forms/components/forms-screen.tsx +6 -3
  88. package/blocks/inbox/README.md +40 -4
  89. package/blocks/inbox/block.json +130 -8
  90. package/blocks/inbox/components/blocked-senders.tsx +103 -0
  91. package/blocks/inbox/components/compose-message.tsx +193 -0
  92. package/blocks/inbox/components/inbox-screen.tsx +54 -6
  93. package/blocks/inbox/components/inbox-search.tsx +106 -0
  94. package/blocks/inbox/components/tag-manager.tsx +136 -0
  95. package/blocks/inbox/components/thread-actions.tsx +133 -0
  96. package/blocks/inbox/components/thread-view.tsx +2 -0
  97. package/blocks/inbox/hooks/use-inbox-mailboxes.ts +31 -0
  98. package/blocks/inbox/lib/inbox-manage.ts +227 -0
  99. package/blocks/newsletter/components/newsletter-screen.tsx +3 -1
  100. package/blocks/pages/block.json +10 -1
  101. package/blocks/pages/hooks/use-landing-page-paths.ts +60 -0
  102. package/blocks/programmatic-pages/README.md +82 -0
  103. package/blocks/programmatic-pages/block.json +292 -0
  104. package/blocks/programmatic-pages/components/programmatic-campaign-form.tsx +136 -0
  105. package/blocks/programmatic-pages/components/programmatic-generate.tsx +112 -0
  106. package/blocks/programmatic-pages/components/programmatic-generated-pages.tsx +210 -0
  107. package/blocks/programmatic-pages/components/programmatic-overview.tsx +84 -0
  108. package/blocks/programmatic-pages/components/programmatic-pages-screen.tsx +221 -0
  109. package/blocks/programmatic-pages/components/programmatic-preview.tsx +66 -0
  110. package/blocks/programmatic-pages/components/programmatic-publish.tsx +102 -0
  111. package/blocks/programmatic-pages/components/programmatic-settings.tsx +96 -0
  112. package/blocks/programmatic-pages/components/programmatic-source-fields.tsx +75 -0
  113. package/blocks/programmatic-pages/components/programmatic-source.tsx +106 -0
  114. package/blocks/programmatic-pages/components/programmatic-variables.tsx +84 -0
  115. package/blocks/programmatic-pages/hooks/use-programmatic-workspace.ts +19 -0
  116. package/blocks/programmatic-pages/lib/programmatic-types.ts +330 -0
  117. package/blocks/programmatic-pages/recipes/create-a-programmatic-campaign.ts +23 -0
  118. package/blocks/programmatic-pages/recipes/render-and-follow.ts +21 -0
  119. package/blocks/programmatic-pages/recipes/review-then-publish.ts +17 -0
  120. package/blocks/quotes/README.md +25 -1
  121. package/blocks/quotes/block.json +123 -6
  122. package/blocks/quotes/components/manual-products.tsx +144 -0
  123. package/blocks/quotes/components/quote-batch-actions.tsx +86 -0
  124. package/blocks/quotes/components/quote-documents.tsx +94 -0
  125. package/blocks/quotes/components/quote-evidence.tsx +95 -0
  126. package/blocks/quotes/components/quote-list.tsx +23 -5
  127. package/blocks/quotes/components/quote-settings.tsx +184 -0
  128. package/blocks/quotes/components/quote-templates.tsx +189 -0
  129. package/blocks/quotes/components/quote-viewer.tsx +16 -3
  130. package/blocks/quotes/components/quotes-screen.tsx +15 -5
  131. package/blocks/quotes/lib/quote-admin.ts +264 -0
  132. package/blocks/sequencer/README.md +55 -0
  133. package/blocks/sequencer/block.json +172 -5
  134. package/blocks/sequencer/components/enrollment-withdraw.tsx +71 -0
  135. package/blocks/sequencer/components/sequence-builder.tsx +13 -3
  136. package/blocks/sequencer/components/sequence-campaign-link.tsx +78 -0
  137. package/blocks/sequencer/components/sequence-detail.tsx +6 -0
  138. package/blocks/sequencer/components/sequence-edit-form.tsx +111 -0
  139. package/blocks/sequencer/components/sequence-enrollments.tsx +2 -1
  140. package/blocks/sequencer/components/sequence-manage-actions.tsx +119 -0
  141. package/blocks/sequencer/components/sequence-senders-panel.tsx +84 -0
  142. package/blocks/sequencer/components/sequence-templates.tsx +160 -0
  143. package/blocks/sequencer/components/sequences-screen.tsx +36 -6
  144. package/blocks/sequencer/components/step-ai-assist.tsx +196 -0
  145. package/blocks/sequencer/components/step-editor.tsx +10 -1
  146. package/blocks/sequencer/components/step-templates.tsx +224 -0
  147. package/blocks/sequencer/lib/sequencer-manage.ts +235 -0
  148. package/blocks/sequencer/lib/sequencer-utils.ts +4 -0
  149. package/blocks/signals/README.md +39 -0
  150. package/blocks/signals/block.json +287 -0
  151. package/blocks/signals/components/auto-seed-panel.tsx +111 -0
  152. package/blocks/signals/components/listener-form.tsx +184 -0
  153. package/blocks/signals/components/matched-posts.tsx +195 -0
  154. package/blocks/signals/components/post-engagements.tsx +114 -0
  155. package/blocks/signals/components/record-social-tab.tsx +49 -0
  156. package/blocks/signals/components/social-listeners-screen.tsx +250 -0
  157. package/blocks/signals/components/social-settings.tsx +128 -0
  158. package/blocks/signals/components/source-lists.tsx +83 -0
  159. package/blocks/signals/components/top-engagers.tsx +68 -0
  160. package/blocks/signals/components/x-integration.tsx +129 -0
  161. package/blocks/signals/lib/signals-types.ts +228 -0
  162. package/blocks/signals/recipes/preview-and-create-a-listener.ts +16 -0
  163. package/blocks/signals/recipes/spend-against-caps.ts +14 -0
  164. package/blocks/signals/recipes/triage-matched-posts.ts +18 -0
  165. package/blocks/visitors/components/traffic-chart.tsx +3 -2
  166. package/blocks/web-analytics/README.md +102 -0
  167. package/blocks/web-analytics/block.json +292 -0
  168. package/blocks/web-analytics/components/acquisition-tab.tsx +154 -0
  169. package/blocks/web-analytics/components/analytics-filters.tsx +161 -0
  170. package/blocks/web-analytics/components/audience-tab.tsx +135 -0
  171. package/blocks/web-analytics/components/conversions-tab.tsx +128 -0
  172. package/blocks/web-analytics/components/engagement-tab.tsx +123 -0
  173. package/blocks/web-analytics/components/overview-tab.tsx +102 -0
  174. package/blocks/web-analytics/components/pages-tab.tsx +148 -0
  175. package/blocks/web-analytics/components/ranked-table.tsx +103 -0
  176. package/blocks/web-analytics/components/realtime-tab.tsx +103 -0
  177. package/blocks/web-analytics/components/report-card.tsx +71 -0
  178. package/blocks/web-analytics/components/web-analytics-screen.tsx +90 -0
  179. package/blocks/web-analytics/hooks/use-report.ts +48 -0
  180. package/blocks/web-analytics/lib/analytics-scope.ts +155 -0
  181. package/blocks/web-analytics/lib/page-path.ts +14 -0
  182. package/blocks/web-analytics/lib/web-analytics-types.ts +187 -0
  183. package/blocks/web-analytics/recipes/best-landing-pages.ts +26 -0
  184. package/blocks/web-analytics/recipes/form-conversion-check.ts +19 -0
  185. package/blocks/web-analytics/recipes/weekly-traffic-report.ts +37 -0
  186. package/blocks/workbench/README.md +54 -2
  187. package/blocks/workbench/block.json +234 -5
  188. package/blocks/workbench/components/workbench-auto-enrich.tsx +88 -0
  189. package/blocks/workbench/components/workbench-automation.tsx +36 -0
  190. package/blocks/workbench/components/workbench-connections.tsx +136 -0
  191. package/blocks/workbench/components/workbench-crm-import.tsx +91 -0
  192. package/blocks/workbench/components/workbench-datafile-import.tsx +154 -0
  193. package/blocks/workbench/components/workbench-grid.tsx +70 -7
  194. package/blocks/workbench/components/workbench-pipelines.tsx +173 -0
  195. package/blocks/workbench/components/workbench-row-actions.tsx +8 -1
  196. package/blocks/workbench/components/workbench-schedule.tsx +191 -0
  197. package/blocks/workbench/components/workbench-screen.tsx +50 -17
  198. package/blocks/workbench/components/workbench-send-to-sequence.tsx +116 -0
  199. package/blocks/workbench/components/workbench-table-settings.tsx +53 -0
  200. package/blocks/workbench/components/workbench-triggers.tsx +178 -0
  201. package/blocks/workbench/hooks/use-connected-crms.ts +29 -0
  202. package/blocks/workbench/lib/api-status.ts +8 -0
  203. package/blocks/workbench/lib/automation-types.ts +257 -0
  204. package/dist/cli.js +257 -51
  205. package/dist/index.browser.js +1 -1
  206. package/dist/index.browser.js.map +1 -1
  207. package/dist/index.browser.mjs +1 -1
  208. package/dist/index.browser.mjs.map +1 -1
  209. package/dist/index.js +1 -1
  210. package/dist/index.js.map +1 -1
  211. package/dist/index.mjs +1 -1
  212. package/dist/index.mjs.map +1 -1
  213. package/dist/react.browser.js +1 -1
  214. package/dist/react.browser.js.map +1 -1
  215. package/dist/react.browser.mjs +1 -1
  216. package/dist/react.browser.mjs.map +1 -1
  217. package/dist/react.js +1 -1
  218. package/dist/react.js.map +1 -1
  219. package/dist/react.mjs +1 -1
  220. package/dist/react.mjs.map +1 -1
  221. package/dist/types/blocks/auth.d.ts +45 -0
  222. package/dist/types/blocks/create.d.ts +5 -2
  223. package/dist/types/version.d.ts +2 -2
  224. package/docs/api/forms.md +1 -1
  225. package/package.json +1 -1
  226. package/stacks/6sense/pages/contacts.tsx.tmpl +2 -0
  227. package/stacks/6sense.json +20 -0
  228. package/stacks/apollo/pages/calls.tsx.tmpl +9 -9
  229. package/stacks/apollo/pages/campaigns.tsx.tmpl +27 -0
  230. package/stacks/apollo/pages/contacts.tsx.tmpl +6 -0
  231. package/stacks/apollo/pages/inbox-contact.tsx.tmpl +3 -3
  232. package/stacks/apollo/pages/lists.tsx.tmpl +14 -2
  233. package/stacks/apollo/pages/sequences.tsx.tmpl +8 -2
  234. package/stacks/apollo.json +33 -2
  235. package/stacks/clay/pages/contacts.tsx.tmpl +2 -0
  236. package/stacks/clay/pages/lists.tsx.tmpl +14 -2
  237. package/stacks/gong/pages/calls.tsx.tmpl +9 -9
  238. package/stacks/gong/pages/contacts.tsx.tmpl +2 -0
  239. package/stacks/hootsuite/pages/contacts.tsx.tmpl +2 -0
  240. package/stacks/hootsuite.json +10 -0
  241. package/stacks/hubspot/pages/campaigns.tsx.tmpl +30 -0
  242. package/stacks/hubspot/pages/companies.tsx.tmpl +2 -0
  243. package/stacks/hubspot/pages/contacts.tsx.tmpl +6 -0
  244. package/stacks/hubspot/pages/deals.tsx.tmpl +4 -0
  245. package/stacks/hubspot/pages/forms.tsx.tmpl +36 -0
  246. package/stacks/hubspot/pages/inbox.tsx.tmpl +2 -2
  247. package/stacks/hubspot/pages/sequences.tsx.tmpl +8 -2
  248. package/stacks/hubspot/pages/web-analytics.tsx.tmpl +14 -0
  249. package/stacks/hubspot.json +45 -2
  250. package/stacks/intercom/pages/inbox-contact.tsx.tmpl +3 -3
  251. package/stacks/intercom.json +10 -0
  252. package/stacks/mailchimp/pages/contacts.tsx.tmpl +2 -0
  253. package/stacks/mailchimp/pages/forms.tsx.tmpl +36 -0
  254. package/stacks/mailchimp.json +2 -1
  255. package/stacks/outreach/pages/calls.tsx.tmpl +9 -9
  256. package/stacks/outreach/pages/campaigns.tsx.tmpl +27 -0
  257. package/stacks/outreach/pages/contacts.tsx.tmpl +2 -0
  258. package/stacks/outreach/pages/inbox-contact.tsx.tmpl +3 -3
  259. package/stacks/outreach/pages/sequences.tsx.tmpl +8 -2
  260. package/stacks/outreach.json +13 -2
  261. package/stacks/pipedrive/pages/contacts.tsx.tmpl +2 -0
  262. package/stacks/pipedrive/pages/inbox.tsx.tmpl +2 -2
  263. package/stacks/pipedrive.json +10 -0
  264. package/stacks/salesforce/pages/companies.tsx.tmpl +2 -0
  265. package/stacks/salesforce/pages/contacts.tsx.tmpl +6 -0
  266. package/stacks/salesforce/pages/deals.tsx.tmpl +4 -0
  267. package/stacks/salesforce.json +10 -0
  268. package/stacks/zoominfo/pages/contacts.tsx.tmpl +4 -0
  269. package/stacks/zoominfo/pages/lists.tsx.tmpl +14 -2
  270. package/stacks/zoominfo.json +20 -0
  271. package/styles/styles.css +1 -1
  272. package/templates/auth/auth-route.ts.tmpl +16 -0
  273. package/templates/auth/g8.auth.anonymous.ts.tmpl +15 -0
  274. package/templates/auth/g8.auth.basic.ts.tmpl +17 -0
  275. package/templates/auth/g8.auth.jwt.ts.tmpl +23 -0
  276. package/templates/auth/g8.auth.password.ts.tmpl +18 -0
  277. package/templates/auth/g8.auth.session.ts.tmpl +23 -0
  278. package/templates/auth/middleware.ts.tmpl +14 -0
  279. package/templates/next-app/README.md.tmpl +5 -0
@@ -0,0 +1,292 @@
1
+ {
2
+ "$schema": "../block.schema.json",
3
+ "id": "web-analytics",
4
+ "title": "Web analytics",
5
+ "summary": "Website analytics for your own sites: traffic, acquisition, audience, pages, engagement, conversions and realtime, with one site picker and window shared across tabs.",
6
+ "features": [
7
+ "one site picker and one date window (7/30/90/365 days or a validated custom range) shared by every tab through the URL",
8
+ "overview, acquisition (channels, sources, UTM campaigns, referrals, social, search), audience (geo, languages, technology), pages, engagement, conversions and realtime",
9
+ "page detail with an optional open-landing-page link by id",
10
+ "loading state on every filter change, an honest unavailable state when the query failed, dashes for unknown rates"
11
+ ],
12
+ "dependsOn": [
13
+ "visitors"
14
+ ],
15
+ "operations": [
16
+ "get_acquisition_campaigns_analytics_acquisition_campaigns_get",
17
+ "get_acquisition_channels_analytics_acquisition_channels_get",
18
+ "get_acquisition_channels_trend_analytics_acquisition_channels_trend_get",
19
+ "get_acquisition_referrals_analytics_acquisition_referrals_get",
20
+ "get_acquisition_search_analytics_acquisition_search_get",
21
+ "get_acquisition_social_analytics_acquisition_social_get",
22
+ "get_acquisition_sources_analytics_acquisition_sources_get",
23
+ "get_audience_behavior_analytics_audience_behavior_get",
24
+ "get_audience_demographics_analytics_audience_demographics_get",
25
+ "get_audience_languages_analytics_audience_languages_get",
26
+ "get_audience_overview_analytics_audience_overview_get",
27
+ "get_audience_trend_analytics_audience_trend_get",
28
+ "get_conversion_rate_analytics_conversions_rate_get",
29
+ "get_engagement_by_campaign_analytics_engagement_by_campaign_get",
30
+ "get_engagement_by_channel_analytics_engagement_by_channel_get",
31
+ "get_engagement_by_landing_page_analytics_engagement_by_landing_page_get",
32
+ "get_engagement_overview_analytics_engagement_overview_get",
33
+ "get_engagement_trend_analytics_engagement_trend_get",
34
+ "get_events_analytics_behavior_events_get",
35
+ "get_exit_pages_analytics_behavior_exit_pages_get",
36
+ "get_form_analysis_analytics_conversions_forms_analysis_get",
37
+ "get_forms_analytics_conversions_forms_get",
38
+ "get_forms_trend_analytics_conversions_forms_trend_get",
39
+ "get_funnel_analytics_conversions_funnel_get",
40
+ "get_geo_cities_analytics_audience_geo_cities_get",
41
+ "get_geo_countries_analytics_audience_geo_get",
42
+ "get_goals_analytics_conversions_goals_get",
43
+ "get_goals_trend_analytics_conversions_goals_trend_get",
44
+ "get_landing_pages_analytics_behavior_landing_pages_get",
45
+ "get_page_metrics_analytics_behavior_page_metrics_get",
46
+ "get_realtime_analytics_realtime_get",
47
+ "get_realtime_locations_analytics_realtime_locations_get",
48
+ "get_realtime_pages_analytics_realtime_pages_get",
49
+ "get_realtime_sources_analytics_realtime_sources_get",
50
+ "get_session_depth_distribution_analytics_engagement_depth_distribution_get",
51
+ "get_technology_overview_analytics_audience_technology_get",
52
+ "get_time_on_site_distribution_analytics_engagement_time_distribution_get",
53
+ "get_top_pages_analytics_behavior_pages_get",
54
+ "list_org_streams_streams_get"
55
+ ],
56
+ "components": [
57
+ {
58
+ "name": "WebAnalyticsScreen",
59
+ "file": "components/web-analytics-screen.tsx",
60
+ "purpose": "Finished website analytics page: seven tabs (overview, acquisition, audience, pages, engagement, conversions, realtime) sharing one site picker and one window through the URL.",
61
+ "operations": [],
62
+ "tiers": []
63
+ },
64
+ {
65
+ "name": "AnalyticsFilters",
66
+ "file": "components/analytics-filters.tsx",
67
+ "purpose": "The one site picker, window (7/30/90/365 days or a validated custom range) and bot/internal traffic switches.",
68
+ "operations": [
69
+ "list_org_streams_streams_get"
70
+ ],
71
+ "tiers": [
72
+ "read"
73
+ ]
74
+ },
75
+ {
76
+ "name": "OverviewTab",
77
+ "file": "components/overview-tab.tsx",
78
+ "purpose": "Audience tiles, daily traffic chart, conversion rate and top pages.",
79
+ "operations": [
80
+ "get_audience_overview_analytics_audience_overview_get",
81
+ "get_audience_trend_analytics_audience_trend_get",
82
+ "get_conversion_rate_analytics_conversions_rate_get",
83
+ "get_top_pages_analytics_behavior_pages_get"
84
+ ],
85
+ "tiers": [
86
+ "read"
87
+ ]
88
+ },
89
+ {
90
+ "name": "AcquisitionTab",
91
+ "file": "components/acquisition-tab.tsx",
92
+ "purpose": "Channels, channel trend, sources and mediums, UTM campaigns, referrals, social networks and search engines.",
93
+ "operations": [
94
+ "get_acquisition_campaigns_analytics_acquisition_campaigns_get",
95
+ "get_acquisition_channels_analytics_acquisition_channels_get",
96
+ "get_acquisition_channels_trend_analytics_acquisition_channels_trend_get",
97
+ "get_acquisition_referrals_analytics_acquisition_referrals_get",
98
+ "get_acquisition_search_analytics_acquisition_search_get",
99
+ "get_acquisition_social_analytics_acquisition_social_get",
100
+ "get_acquisition_sources_analytics_acquisition_sources_get"
101
+ ],
102
+ "tiers": [
103
+ "read"
104
+ ]
105
+ },
106
+ {
107
+ "name": "AudienceTab",
108
+ "file": "components/audience-tab.tsx",
109
+ "purpose": "Demographics at a glance, countries and cities, languages, technology, new or returning visitors.",
110
+ "operations": [
111
+ "get_audience_behavior_analytics_audience_behavior_get",
112
+ "get_audience_demographics_analytics_audience_demographics_get",
113
+ "get_audience_languages_analytics_audience_languages_get",
114
+ "get_geo_cities_analytics_audience_geo_cities_get",
115
+ "get_geo_countries_analytics_audience_geo_get",
116
+ "get_technology_overview_analytics_audience_technology_get"
117
+ ],
118
+ "tiers": [
119
+ "read"
120
+ ]
121
+ },
122
+ {
123
+ "name": "PagesTab",
124
+ "file": "components/pages-tab.tsx",
125
+ "purpose": "Top, landing and exit pages, custom events, and a page detail panel; a page can open its landing page by id.",
126
+ "operations": [
127
+ "get_events_analytics_behavior_events_get",
128
+ "get_exit_pages_analytics_behavior_exit_pages_get",
129
+ "get_landing_pages_analytics_behavior_landing_pages_get",
130
+ "get_page_metrics_analytics_behavior_page_metrics_get",
131
+ "get_top_pages_analytics_behavior_pages_get"
132
+ ],
133
+ "tiers": [
134
+ "read"
135
+ ]
136
+ },
137
+ {
138
+ "name": "EngagementTab",
139
+ "file": "components/engagement-tab.tsx",
140
+ "purpose": "Sessions, depth, time on site, bounce and engaged share against the previous period, distributions, by channel, campaign and landing page.",
141
+ "operations": [
142
+ "get_engagement_by_campaign_analytics_engagement_by_campaign_get",
143
+ "get_engagement_by_channel_analytics_engagement_by_channel_get",
144
+ "get_engagement_by_landing_page_analytics_engagement_by_landing_page_get",
145
+ "get_engagement_overview_analytics_engagement_overview_get",
146
+ "get_engagement_trend_analytics_engagement_trend_get",
147
+ "get_session_depth_distribution_analytics_engagement_depth_distribution_get",
148
+ "get_time_on_site_distribution_analytics_engagement_time_distribution_get"
149
+ ],
150
+ "tiers": [
151
+ "read"
152
+ ]
153
+ },
154
+ {
155
+ "name": "ConversionsTab",
156
+ "file": "components/conversions-tab.tsx",
157
+ "purpose": "Conversion rate, funnel, goals and their trend, forms, form trend and form analysis.",
158
+ "operations": [
159
+ "get_conversion_rate_analytics_conversions_rate_get",
160
+ "get_form_analysis_analytics_conversions_forms_analysis_get",
161
+ "get_forms_analytics_conversions_forms_get",
162
+ "get_forms_trend_analytics_conversions_forms_trend_get",
163
+ "get_funnel_analytics_conversions_funnel_get",
164
+ "get_goals_analytics_conversions_goals_get",
165
+ "get_goals_trend_analytics_conversions_goals_trend_get"
166
+ ],
167
+ "tiers": [
168
+ "read"
169
+ ]
170
+ },
171
+ {
172
+ "name": "RealtimeTab",
173
+ "file": "components/realtime-tab.tsx",
174
+ "purpose": "Active visitors now, active pages, traffic sources and locations; polled while visible.",
175
+ "operations": [
176
+ "get_realtime_analytics_realtime_get",
177
+ "get_realtime_locations_analytics_realtime_locations_get",
178
+ "get_realtime_pages_analytics_realtime_pages_get",
179
+ "get_realtime_sources_analytics_realtime_sources_get"
180
+ ],
181
+ "tiers": [
182
+ "read"
183
+ ]
184
+ },
185
+ {
186
+ "name": "ReportCard",
187
+ "file": "components/report-card.tsx",
188
+ "purpose": "A report in a card with loading, error, unavailable (stale), empty and dimmed-while-refreshing states.",
189
+ "operations": [],
190
+ "tiers": []
191
+ },
192
+ {
193
+ "name": "RankedTable",
194
+ "file": "components/ranked-table.tsx",
195
+ "purpose": "Re-sortable top-N table that sorts the whole list before cutting it, with a first-N notice.",
196
+ "operations": [],
197
+ "tiers": []
198
+ }
199
+ ],
200
+ "screens": [
201
+ {
202
+ "component": "WebAnalyticsScreen",
203
+ "path": "/web-analytics",
204
+ "label": "Web analytics",
205
+ "icon": "ChartNoAxesCombined",
206
+ "group": "Signals"
207
+ }
208
+ ],
209
+ "hooks": [
210
+ {
211
+ "name": "useReport",
212
+ "file": "hooks/use-report.ts",
213
+ "purpose": "One report read with first-load, pending (scope changed), error and stale states.",
214
+ "operations": []
215
+ }
216
+ ],
217
+ "lib": [
218
+ {
219
+ "file": "lib/web-analytics-types.ts",
220
+ "purpose": "Row types typed from the backend queries (the contract leaves them untyped), envelope readers and dash-safe formatters."
221
+ },
222
+ {
223
+ "file": "lib/analytics-scope.ts",
224
+ "purpose": "The shared scope (sites, window, traffic switches) in the URL, with the checks the API enforces and each report's limit cap."
225
+ },
226
+ {
227
+ "file": "lib/page-path.ts",
228
+ "purpose": "The page path pattern the single-page report accepts."
229
+ }
230
+ ],
231
+ "recipes": [
232
+ {
233
+ "file": "recipes/weekly-traffic-report.ts",
234
+ "title": "Summarise a site's traffic, sources and conversion for a window",
235
+ "when": "A weekly digest or dashboard header."
236
+ },
237
+ {
238
+ "file": "recipes/best-landing-pages.ts",
239
+ "title": "Rank landing pages by sessions and engagement",
240
+ "when": "Deciding which landing pages to promote or rewrite."
241
+ },
242
+ {
243
+ "file": "recipes/form-conversion-check.ts",
244
+ "title": "Find forms that get views but few submissions",
245
+ "when": "A conversion review."
246
+ }
247
+ ],
248
+ "prerequisites": [
249
+ {
250
+ "id": "snippet-on-site",
251
+ "description": "The graph8 tracking snippet is pasted into the website. Without it every report is empty.",
252
+ "where": "app",
253
+ "verify": "GET /visitors/installation-check reports status receiving (the visitors block's SnippetInstall)."
254
+ },
255
+ {
256
+ "id": "website-stream",
257
+ "description": "The workspace has at least one website stream; its write key is what the site picker filters on.",
258
+ "where": "api",
259
+ "verify": "GET /streams returns at least one stream with a write_key."
260
+ }
261
+ ],
262
+ "scopes": [
263
+ "analytics:read",
264
+ "integrations:read"
265
+ ],
266
+ "defaults": [
267
+ {
268
+ "decision": "Window",
269
+ "default": "Last 30 days",
270
+ "askHuman": false
271
+ },
272
+ {
273
+ "decision": "Sites",
274
+ "default": "All sites (no filter)",
275
+ "askHuman": false
276
+ },
277
+ {
278
+ "decision": "Bots and internal traffic",
279
+ "default": "Excluded (the API's default)",
280
+ "askHuman": false
281
+ },
282
+ {
283
+ "decision": "Realtime refresh",
284
+ "default": "Every 10 seconds while the page is visible",
285
+ "askHuman": false
286
+ }
287
+ ],
288
+ "peerDependencies": {
289
+ "react": ">=18",
290
+ "lucide-react": ">=0.400"
291
+ }
292
+ }
@@ -0,0 +1,154 @@
1
+ "use client";
2
+
3
+ /**
4
+ * AcquisitionTab: where visitors come from.
5
+ * Channels sessions and bounce rate per channel group (Direct, Organic Search, Paid Search, Email, ...)
6
+ * Channel trend sessions per day, for all channels or one chosen channel
7
+ * Sources and mediums / Campaigns (UTM) / Referrals ranked tables you can re-sort; sorting ranks the whole list, then cuts it
8
+ * Social / Search engines the networks and engines that sent visitors
9
+ *
10
+ * SDK operations, in order (all read, free). RankedTable renders a FirstNotice ("Showing the first N") when a read came back full:
11
+ * 1. getAcquisitionChannels GET /analytics/acquisition/channels
12
+ * 2. getAcquisitionChannelsTrend GET /analytics/acquisition/channels/trend
13
+ * 3. getAcquisitionSources GET /analytics/acquisition/sources (limit 200)
14
+ * 4. getAcquisitionCampaigns GET /analytics/acquisition/campaigns (limit 200)
15
+ * 5. getAcquisitionReferrals GET /analytics/acquisition/referrals (limit 200)
16
+ * 6. getAcquisitionSocial GET /analytics/acquisition/social
17
+ * 7. getAcquisitionSearch GET /analytics/acquisition/search
18
+ * Types: see lib/web-analytics-types.ts (the contract leaves these payloads untyped).
19
+ */
20
+ import { useMemo, useState } from "react";
21
+ import { BarList } from "@/g8/components/screen/bar-list";
22
+ import { TimeSeriesChart } from "@/g8/components/screen/time-series-chart";
23
+ import { g8Get } from "@/g8/lib/g8-client";
24
+ import { useReport } from "../hooks/use-report";
25
+ import { LIMITS, windowedQuery } from "../lib/analytics-scope";
26
+ import type { ScopeState } from "../lib/analytics-scope";
27
+ import { fmtDay, fmtInt, fmtPct, num, readRows, str } from "../lib/web-analytics-types";
28
+ import type { CampaignRow, Channel, ChannelTrendPoint, Referral, Row, SourceMedium } from "../lib/web-analytics-types";
29
+ import { RankedTable } from "./ranked-table";
30
+ import { ReportCard } from "./report-card";
31
+
32
+ const SELECT = "h-8 rounded-md border bg-background px-2 text-sm";
33
+
34
+ function ChannelTrend({ rows }: { rows: ChannelTrendPoint[] }) {
35
+ const [channel, setChannel] = useState("");
36
+ const channels = useMemo(() => [...new Set(rows.map((r) => str(r.channel)).filter(Boolean))].sort(), [rows]);
37
+ const data = useMemo(() => {
38
+ const byDay = new Map<string, number>();
39
+ for (const r of rows) {
40
+ if (channel && str(r.channel) !== channel) continue;
41
+ byDay.set(str(r.date), (byDay.get(str(r.date)) ?? 0) + (num(r.sessions) ?? 0));
42
+ }
43
+ return [...byDay.entries()].sort(([a], [b]) => a.localeCompare(b)).map(([date, sessions]) => ({ date, sessions }));
44
+ }, [rows, channel]);
45
+ return (
46
+ <div className="space-y-2">
47
+ <select aria-label="Channel" className={SELECT} value={channel} onChange={(e) => setChannel(e.target.value)}>
48
+ <option value="">All channels</option>
49
+ {channels.map((c) => <option key={c} value={c}>{c}</option>)}
50
+ </select>
51
+ <TimeSeriesChart data={data} xKey="date" series={[{ key: "sessions", label: "Sessions" }]} kind="bar" height={200} formatX={fmtDay} formatY={(v) => v.toLocaleString()} />
52
+ </div>
53
+ );
54
+ }
55
+
56
+ function NameBars({ rows, name, value }: { rows: Row[]; name: string; value: string }) {
57
+ return <BarList items={rows.map((r) => ({ label: str(r[name]) || "Unknown", value: num(r[value]) ?? 0 }))} />;
58
+ }
59
+
60
+ export function AcquisitionTab({ scope }: { scope: ScopeState }) {
61
+ const q = windowedQuery(scope);
62
+ const channels = useReport(async (signal) => readRows<Channel>(await g8Get("/analytics/acquisition/channels", q, { signal })), scope.key);
63
+ const trend = useReport(async (signal) => readRows<ChannelTrendPoint>(await g8Get("/analytics/acquisition/channels/trend", q, { signal })), scope.key);
64
+ const sources = useReport(async (signal) => readRows<SourceMedium>(await g8Get("/analytics/acquisition/sources", { ...q, limit: LIMITS.sources }, { signal })), scope.key);
65
+ const campaigns = useReport(async (signal) => readRows<CampaignRow>(await g8Get("/analytics/acquisition/campaigns", { ...q, limit: LIMITS.campaigns }, { signal })), scope.key);
66
+ const referrals = useReport(async (signal) => readRows<Referral>(await g8Get("/analytics/acquisition/referrals", { ...q, limit: LIMITS.referrals }, { signal })), scope.key);
67
+ const social = useReport(async (signal) => readRows(await g8Get("/analytics/acquisition/social", q, { signal })), scope.key);
68
+ const search = useReport(async (signal) => readRows(await g8Get("/analytics/acquisition/search", q, { signal })), scope.key);
69
+ const none = (rows: unknown[]) => rows.length === 0;
70
+ return (
71
+ <div className="grid gap-4 lg:grid-cols-2">
72
+ <ReportCard title="Channels" description="Sessions by channel group" report={channels} isEmpty={none}>
73
+ {(rows) => (
74
+ <RankedTable
75
+ rows={rows}
76
+ noun="channels"
77
+ defaultSort="sessions"
78
+ rowKey={(r, i) => str(r.channel) || String(i)}
79
+ columns={[
80
+ { key: "channel", label: "Channel", render: (r) => str(r.channel) || "Unknown", sortBy: (r) => str(r.channel) },
81
+ { key: "users", label: "Visitors", numeric: true, render: (r) => fmtInt(r.users), sortBy: (r) => num(r.users) },
82
+ { key: "sessions", label: "Sessions", numeric: true, render: (r) => fmtInt(r.sessions), sortBy: (r) => num(r.sessions) },
83
+ { key: "page_views", label: "Page views", numeric: true, render: (r) => fmtInt(r.page_views), sortBy: (r) => num(r.page_views) },
84
+ { key: "bounce", label: "Bounce rate", numeric: true, render: (r) => fmtPct(r.bounce_rate), sortBy: (r) => num(r.bounce_rate) },
85
+ ]}
86
+ />
87
+ )}
88
+ </ReportCard>
89
+ <ReportCard title="Channel trend" description="Sessions per day" report={trend} isEmpty={none} skeletonHeight={240}>
90
+ {(rows) => <ChannelTrend rows={rows} />}
91
+ </ReportCard>
92
+ <ReportCard className="lg:col-span-2" title="Sources and mediums" report={sources} isEmpty={none}>
93
+ {(rows) => (
94
+ <RankedTable
95
+ rows={rows}
96
+ noun="sources"
97
+ limit={LIMITS.sources}
98
+ defaultSort="sessions"
99
+ rowKey={(r, i) => `${str(r.source)}|${str(r.medium)}|${i}`}
100
+ columns={[
101
+ { key: "source", label: "Source", render: (r) => str(r.source) || "Unknown", sortBy: (r) => str(r.source) },
102
+ { key: "medium", label: "Medium", render: (r) => str(r.medium) || "Unknown", sortBy: (r) => str(r.medium) },
103
+ { key: "users", label: "Visitors", numeric: true, render: (r) => fmtInt(r.users), sortBy: (r) => num(r.users) },
104
+ { key: "sessions", label: "Sessions", numeric: true, render: (r) => fmtInt(r.sessions), sortBy: (r) => num(r.sessions) },
105
+ { key: "page_views", label: "Page views", numeric: true, render: (r) => fmtInt(r.page_views), sortBy: (r) => num(r.page_views) },
106
+ ]}
107
+ />
108
+ )}
109
+ </ReportCard>
110
+ <ReportCard className="lg:col-span-2" title="Campaigns" description="From the utm_campaign tag" report={campaigns} isEmpty={none} emptyTitle="No tagged campaigns in this window" emptyDescription="Add utm_campaign to your links and they appear here.">
111
+ {(rows) => (
112
+ <RankedTable
113
+ rows={rows}
114
+ noun="campaigns"
115
+ limit={LIMITS.campaigns}
116
+ defaultSort="sessions"
117
+ rowKey={(r, i) => `${str(r.campaign)}|${str(r.source)}|${i}`}
118
+ columns={[
119
+ { key: "campaign", label: "Campaign", render: (r) => str(r.campaign) || "Unknown", sortBy: (r) => str(r.campaign) },
120
+ { key: "source", label: "Source", render: (r) => str(r.source), sortBy: (r) => str(r.source) },
121
+ { key: "medium", label: "Medium", render: (r) => str(r.medium), sortBy: (r) => str(r.medium) },
122
+ { key: "users", label: "Visitors", numeric: true, render: (r) => fmtInt(r.users), sortBy: (r) => num(r.users) },
123
+ { key: "sessions", label: "Sessions", numeric: true, render: (r) => fmtInt(r.sessions), sortBy: (r) => num(r.sessions) },
124
+ { key: "page_views", label: "Page views", numeric: true, render: (r) => fmtInt(r.page_views), sortBy: (r) => num(r.page_views) },
125
+ ]}
126
+ />
127
+ )}
128
+ </ReportCard>
129
+ <ReportCard className="lg:col-span-2" title="Referrals" description="Other websites that sent visitors" report={referrals} isEmpty={none}>
130
+ {(rows) => (
131
+ <RankedTable
132
+ rows={rows}
133
+ noun="referrers"
134
+ limit={LIMITS.referrals}
135
+ defaultSort="sessions"
136
+ rowKey={(r, i) => `${str(r.referrer)}|${i}`}
137
+ columns={[
138
+ { key: "referrer", label: "Referrer", render: (r) => str(r.referrer) || "Unknown", sortBy: (r) => str(r.referrer) },
139
+ { key: "users", label: "Visitors", numeric: true, render: (r) => fmtInt(r.users), sortBy: (r) => num(r.users) },
140
+ { key: "sessions", label: "Sessions", numeric: true, render: (r) => fmtInt(r.sessions), sortBy: (r) => num(r.sessions) },
141
+ { key: "page_views", label: "Page views", numeric: true, render: (r) => fmtInt(r.page_views), sortBy: (r) => num(r.page_views) },
142
+ ]}
143
+ />
144
+ )}
145
+ </ReportCard>
146
+ <ReportCard title="Social networks" description="Visitors by network" report={social} isEmpty={none} emptyTitle="No social visits in this window">
147
+ {(rows) => <NameBars rows={rows} name="network" value="users" />}
148
+ </ReportCard>
149
+ <ReportCard title="Search engines" description="Visitors by engine" report={search} isEmpty={none} emptyTitle="No search visits in this window">
150
+ {(rows) => <NameBars rows={rows} name="search_engine" value="users" />}
151
+ </ReportCard>
152
+ </div>
153
+ );
154
+ }
@@ -0,0 +1,161 @@
1
+ "use client";
2
+
3
+ /**
4
+ * AnalyticsFilters: the one site picker, window and traffic switches that drive every tab (state lives in the URL, see
5
+ * lib/analytics-scope.ts).
6
+ *
7
+ * Sites every website stream of the workspace, as checkboxes (none checked = all sites). A stream without a write
8
+ * key cannot be filtered on, so it is shown disabled.
9
+ * Window 7 / 30 / 90 / 365 days, or a custom range. The custom range is only applied when the API would accept it
10
+ * (both days, real dates, start not after end, at most 365 days); otherwise Apply is disabled and says why.
11
+ * Traffic "Include bots" and "Include internal traffic" (both off by default, and only sent when on).
12
+ *
13
+ * SDK operations, in order:
14
+ * 1. listOrgStreams GET /streams?page&limit (read; the sites, paged with useAllPages)
15
+ */
16
+ import { useEffect, useRef, useState } from "react";
17
+ import { Globe } from "lucide-react";
18
+ import { Button, Skeleton, cn } from "@graph8/sdk/ui";
19
+ import { useAllPages } from "@/g8/hooks/use-all-pages";
20
+ import { g8Get } from "@/g8/lib/g8-client";
21
+ import { hasNextOf } from "@/g8/lib/paging";
22
+ import { MoreFilters } from "@/g8/components/screen/filter-controls";
23
+ import { DAY_PRESETS, RANGE_PROBLEM_TEXT, rangeProblem, todayUtc } from "../lib/analytics-scope";
24
+ import type { ScopeState } from "../lib/analytics-scope";
25
+
26
+ interface Stream {
27
+ id: string;
28
+ name?: string;
29
+ domains?: string[];
30
+ write_key?: string | null;
31
+ }
32
+
33
+ const STREAM_PAGE = 50;
34
+
35
+ function SitePicker({ scope }: { scope: ScopeState }) {
36
+ const [open, setOpen] = useState(false);
37
+ const box = useRef<HTMLDivElement>(null);
38
+ // Close on a click outside or Escape, like any menu.
39
+ useEffect(() => {
40
+ if (!open) return;
41
+ const away = (e: MouseEvent) => { if (box.current && !box.current.contains(e.target as Node)) setOpen(false); };
42
+ const esc = (e: KeyboardEvent) => { if (e.key === "Escape") setOpen(false); };
43
+ document.addEventListener("mousedown", away);
44
+ document.addEventListener("keydown", esc);
45
+ return () => { document.removeEventListener("mousedown", away); document.removeEventListener("keydown", esc); };
46
+ }, [open]);
47
+ const streams = useAllPages<Stream>(async (page, signal) => {
48
+ const env = await g8Get<Stream[]>("/streams", { page, limit: STREAM_PAGE }, { signal });
49
+ const rows = env.data ?? [];
50
+ return { rows, hasNext: hasNextOf(env, STREAM_PAGE, rows.length) };
51
+ }, []);
52
+ const keyed = streams.items.filter((s) => s.write_key && /^[A-Za-z0-9_-]+$/.test(s.write_key));
53
+ const known = new Set(keyed.map((s) => s.write_key!));
54
+ const unknown = streams.loaded ? scope.sites.filter((k) => !known.has(k)) : [];
55
+ const toggle = (key: string) => scope.setSites(scope.sites.includes(key) ? scope.sites.filter((k) => k !== key) : [...scope.sites, key]);
56
+ const nameOf = (key: string) => keyed.find((s) => s.write_key === key)?.name ?? key;
57
+ const label = scope.sites.length === 0 ? "All sites" : scope.sites.length === 1 ? nameOf(scope.sites[0]) : `${scope.sites.length} sites`;
58
+ return (
59
+ <div className="relative" ref={box}>
60
+ <Button type="button" variant="outline" size="sm" aria-expanded={open} aria-haspopup="true" aria-label={`Sites: ${label}`} onClick={() => setOpen((o) => !o)}>
61
+ <Globe className="size-4" strokeWidth={1.5} aria-hidden />
62
+ {label}
63
+ </Button>
64
+ {open ? (
65
+ <div role="group" aria-label="Sites" className="absolute left-0 top-full z-30 mt-1 w-72 rounded-md border border-border bg-popover p-2 text-sm shadow-md">
66
+ {streams.loading && !streams.loaded ? (
67
+ <Skeleton className="h-16 w-full" />
68
+ ) : streams.error ? (
69
+ <div className="space-y-2 p-1">
70
+ <p className="text-error-foreground">Could not load your sites.</p>
71
+ <Button type="button" size="sm" variant="outline" onClick={streams.refetch}>Retry</Button>
72
+ </div>
73
+ ) : (
74
+ <ul className="max-h-64 space-y-0.5 overflow-auto">
75
+ <li>
76
+ <label className="flex cursor-pointer items-center gap-2 rounded px-2 py-1.5 hover:bg-surface-hover">
77
+ <input type="checkbox" checked={scope.sites.length === 0} onChange={() => scope.setSites([])} />
78
+ <span>All sites</span>
79
+ </label>
80
+ </li>
81
+ {keyed.map((s) => (
82
+ <li key={s.id}>
83
+ <label className="flex cursor-pointer items-center gap-2 rounded px-2 py-1.5 hover:bg-surface-hover">
84
+ <input type="checkbox" checked={scope.sites.includes(s.write_key!)} onChange={() => toggle(s.write_key!)} />
85
+ <span className="min-w-0 truncate">{s.name || s.domains?.[0] || s.write_key}</span>
86
+ {s.domains?.[0] && s.name ? <span className="min-w-0 truncate text-xs text-text-color-muted">{s.domains[0]}</span> : null}
87
+ </label>
88
+ </li>
89
+ ))}
90
+ {streams.items.filter((s) => !keyed.includes(s)).map((s) => (
91
+ <li key={s.id} className="flex items-center gap-2 px-2 py-1.5 text-text-color-muted">
92
+ <input type="checkbox" disabled aria-label={`${s.name || s.id} has no write key`} />
93
+ <span className="truncate">{s.name || s.id}</span>
94
+ <span className="text-xs">no write key</span>
95
+ </li>
96
+ ))}
97
+ {unknown.map((k) => (
98
+ <li key={k}>
99
+ <label className="flex cursor-pointer items-center gap-2 rounded px-2 py-1.5 hover:bg-surface-hover">
100
+ <input type="checkbox" checked onChange={() => toggle(k)} />
101
+ <span className="truncate">{k}</span>
102
+ <span className="text-xs text-text-color-muted">not in this workspace</span>
103
+ </label>
104
+ </li>
105
+ ))}
106
+ {keyed.length === 0 && streams.items.length === 0 ? <li className="px-2 py-1.5 text-text-color-muted">No sites yet. Install the tracking snippet first.</li> : null}
107
+ </ul>
108
+ )}
109
+ {streams.truncated ? <p role="note" className="px-1 pt-1 text-xs text-text-color-muted">Showing the first {streams.items.length} sites.</p> : null}
110
+ </div>
111
+ ) : null}
112
+ </div>
113
+ );
114
+ }
115
+
116
+ function WindowPicker({ scope }: { scope: ScopeState }) {
117
+ const w = scope.window;
118
+ const [custom, setCustom] = useState(w.kind === "range");
119
+ const [from, setFrom] = useState(w.from ?? "");
120
+ const [to, setTo] = useState(w.to ?? "");
121
+ useEffect(() => {
122
+ if (w.kind === "range") { setFrom(w.from ?? ""); setTo(w.to ?? ""); setCustom(true); }
123
+ }, [w.kind, w.from, w.to]);
124
+ const problem = rangeProblem(from, to);
125
+ const today = todayUtc();
126
+ return (
127
+ <div className="flex flex-wrap items-center gap-2">
128
+ <div role="group" aria-label="Window" className="inline-flex gap-1">
129
+ {DAY_PRESETS.map((d) => (
130
+ <Button key={d} type="button" size="sm" variant={w.kind === "days" && w.days === d && !custom ? "default" : "outline"} aria-pressed={w.kind === "days" && w.days === d && !custom} onClick={() => { setCustom(false); scope.setDays(d); }}>
131
+ {d === 365 ? "1 year" : `${d} days`}
132
+ </Button>
133
+ ))}
134
+ <Button type="button" size="sm" variant={custom ? "default" : "outline"} aria-pressed={custom} onClick={() => setCustom((c) => !c)}>Custom</Button>
135
+ </div>
136
+ {custom ? (
137
+ <div role="group" aria-label="Custom range" className="flex flex-wrap items-center gap-1.5">
138
+ <input type="date" aria-label="From day" className="h-8 rounded-md border bg-background px-2 text-sm" value={from} max={to || today} onChange={(e) => setFrom(e.target.value)} />
139
+ <span className="text-xs text-text-color-muted" aria-hidden>to</span>
140
+ <input type="date" aria-label="To day" className="h-8 rounded-md border bg-background px-2 text-sm" value={to} min={from || undefined} max={today} onChange={(e) => setTo(e.target.value)} />
141
+ <Button type="button" size="sm" disabled={problem !== null} onClick={() => scope.setRange(from, to)}>Apply range</Button>
142
+ {problem && (from || to) ? <span role="status" className={cn("text-xs", problem === "incomplete" ? "text-text-color-muted" : "text-error-foreground")}>{RANGE_PROBLEM_TEXT[problem]}</span> : null}
143
+ </div>
144
+ ) : null}
145
+ </div>
146
+ );
147
+ }
148
+
149
+ export function AnalyticsFilters({ scope, showWindow = true }: { scope: ScopeState; showWindow?: boolean }) {
150
+ const hidden = (scope.bots ? 1 : 0) + (scope.internal ? 1 : 0);
151
+ return (
152
+ <div className="flex flex-wrap items-center gap-2" data-testid="analytics-filters">
153
+ <SitePicker scope={scope} />
154
+ {showWindow ? <WindowPicker scope={scope} /> : <span className="text-xs text-text-color-muted">Realtime ignores the window</span>}
155
+ <MoreFilters activeCount={hidden} label="Traffic">
156
+ <label className="flex items-center gap-1.5 text-sm"><input type="checkbox" checked={scope.bots} onChange={(e) => scope.setBots(e.target.checked)} />Include bots and crawlers</label>
157
+ <label className="flex items-center gap-1.5 text-sm"><input type="checkbox" checked={scope.internal} onChange={(e) => scope.setInternal(e.target.checked)} />Include internal traffic</label>
158
+ </MoreFilters>
159
+ </div>
160
+ );
161
+ }