@graph8/sdk 0.308.1 → 0.357.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/README.md +81 -0
- package/blocks/README.md +32 -0
- package/blocks/_foundation/README.md +78 -0
- package/blocks/_foundation/block.json +79 -0
- package/blocks/_foundation/components/api-error.tsx +62 -0
- package/blocks/_foundation/components/confirm-spend-dialog.tsx +142 -0
- package/blocks/_foundation/components/empty-state.tsx +24 -0
- package/blocks/_foundation/hooks/use-g8-mutation.ts +88 -0
- package/blocks/_foundation/hooks/use-g8-query.ts +87 -0
- package/blocks/_foundation/hooks/use-job-poll.ts +130 -0
- package/blocks/_foundation/hooks/use-paginated.ts +105 -0
- package/blocks/_foundation/lib/g8-client.ts +128 -0
- package/blocks/_foundation/recipes/verify-proxy-key.ts +18 -0
- package/blocks/_foundation/server/proxy.ts +233 -0
- package/blocks/_foundation/server/route.ts +30 -0
- package/blocks/agents/README.md +133 -0
- package/blocks/agents/block.json +223 -0
- package/blocks/agents/components/agent-builder.tsx +199 -0
- package/blocks/agents/components/agent-knowledge-panel.tsx +144 -0
- package/blocks/agents/components/agent-runs.tsx +175 -0
- package/blocks/agents/components/agent-skills-panel.tsx +126 -0
- package/blocks/agents/components/agent-test-console.tsx +175 -0
- package/blocks/agents/lib/agent-types.ts +114 -0
- package/blocks/agents/recipes/create-agent-and-attach-skill.ts +50 -0
- package/blocks/agents/recipes/run-and-follow-agent-run.ts +46 -0
- package/blocks/agents/recipes/test-agent-before-activating.ts +36 -0
- package/blocks/analytics/README.md +73 -0
- package/blocks/analytics/block.json +178 -0
- package/blocks/analytics/components/funnel-chart.tsx +79 -0
- package/blocks/analytics/components/kpi-tiles.tsx +74 -0
- package/blocks/analytics/components/leaderboard.tsx +86 -0
- package/blocks/analytics/components/report-builder.tsx +200 -0
- package/blocks/analytics/hooks/use-report.ts +26 -0
- package/blocks/analytics/lib/analytics-types.ts +108 -0
- package/blocks/analytics/lib/use-debounced.ts +16 -0
- package/blocks/analytics/recipes/01-weekly-outbound-report.ts +32 -0
- package/blocks/analytics/recipes/02-revenue-snapshot.ts +22 -0
- package/blocks/analytics/recipes/03-saved-report.ts +30 -0
- package/blocks/appointments/README.md +80 -0
- package/blocks/appointments/block.json +218 -0
- package/blocks/appointments/components/availability-editor.tsx +192 -0
- package/blocks/appointments/components/booking-page.tsx +154 -0
- package/blocks/appointments/components/event-type-editor.tsx +214 -0
- package/blocks/appointments/components/meeting-detail.tsx +147 -0
- package/blocks/appointments/components/meeting-list.tsx +148 -0
- package/blocks/appointments/hooks/use-available-slots.ts +29 -0
- package/blocks/appointments/hooks/use-schedules.ts +18 -0
- package/blocks/appointments/lib/appointments-types.ts +159 -0
- package/blocks/appointments/lib/idempotency.ts +30 -0
- package/blocks/appointments/recipes/01-create-event-type-and-hours.ts +27 -0
- package/blocks/appointments/recipes/02-find-slots-and-book.ts +34 -0
- package/blocks/appointments/recipes/03-bookings-digest.ts +22 -0
- package/blocks/appointments/recipes/04-calendar-invite.ts +21 -0
- package/blocks/block.schema.json +130 -0
- package/blocks/block.types.ts +109 -0
- package/blocks/campaigns/README.md +118 -0
- package/blocks/campaigns/block.json +361 -0
- package/blocks/campaigns/components/campaign-assets.tsx +180 -0
- package/blocks/campaigns/components/campaign-audience-step.tsx +91 -0
- package/blocks/campaigns/components/campaign-launch-step.tsx +147 -0
- package/blocks/campaigns/components/campaign-list.tsx +120 -0
- package/blocks/campaigns/components/campaign-metrics.tsx +93 -0
- package/blocks/campaigns/components/campaign-select.tsx +52 -0
- package/blocks/campaigns/components/campaign-sequence-step.tsx +158 -0
- package/blocks/campaigns/components/campaign-wizard.tsx +238 -0
- package/blocks/campaigns/components/form-field.tsx +29 -0
- package/blocks/campaigns/components/icp-editor.tsx +230 -0
- package/blocks/campaigns/components/persona-editor.tsx +210 -0
- package/blocks/campaigns/hooks/use-company-profile.ts +34 -0
- package/blocks/campaigns/lib/campaigns-utils.ts +155 -0
- package/blocks/campaigns/recipes/attach-audience-and-launch.ts +33 -0
- package/blocks/campaigns/recipes/campaign-report.ts +22 -0
- package/blocks/campaigns/recipes/generate-ideas-and-create-campaign.ts +51 -0
- package/blocks/campaigns/recipes/set-up-icp-and-personas.ts +37 -0
- package/blocks/coach/README.md +93 -0
- package/blocks/coach/block.json +125 -0
- package/blocks/coach/components/call-scorecard.tsx +126 -0
- package/blocks/coach/components/coaching-feed.tsx +140 -0
- package/blocks/coach/lib/coach-types.ts +116 -0
- package/blocks/coach/recipes/read-a-scorecard.ts +27 -0
- package/blocks/coach/recipes/weekly-coaching-digest.ts +53 -0
- package/blocks/copilot/README.md +99 -0
- package/blocks/copilot/block.json +151 -0
- package/blocks/copilot/components/copilot-memory.tsx +87 -0
- package/blocks/copilot/components/copilot-panel.tsx +189 -0
- package/blocks/copilot/components/copilot-team-chat.tsx +111 -0
- package/blocks/copilot/lib/copilot-types.ts +111 -0
- package/blocks/copilot/recipes/ask-the-copilot.ts +35 -0
- package/blocks/copilot/recipes/post-in-a-work-channel.ts +33 -0
- package/blocks/copilot/recipes/remember-and-recall.ts +19 -0
- package/blocks/crm/README.md +122 -0
- package/blocks/crm/block.json +378 -0
- package/blocks/crm/components/activity-timeline.tsx +99 -0
- package/blocks/crm/components/company-detail.tsx +118 -0
- package/blocks/crm/components/company-table.tsx +173 -0
- package/blocks/crm/components/contact-detail.tsx +135 -0
- package/blocks/crm/components/contact-table.tsx +217 -0
- package/blocks/crm/components/dedupe-review.tsx +164 -0
- package/blocks/crm/components/import-wizard.tsx +193 -0
- package/blocks/crm/components/notes-panel.tsx +112 -0
- package/blocks/crm/components/record-form.tsx +191 -0
- package/blocks/crm/components/task-list.tsx +107 -0
- package/blocks/crm/hooks/use-custom-fields.ts +18 -0
- package/blocks/crm/hooks/use-debounced.ts +13 -0
- package/blocks/crm/hooks/use-page-query.ts +52 -0
- package/blocks/crm/lib/crm-types.ts +136 -0
- package/blocks/crm/recipes/custom-fields-and-values.ts +28 -0
- package/blocks/crm/recipes/dedupe-and-merge.ts +30 -0
- package/blocks/crm/recipes/notes-tasks-and-activity.ts +23 -0
- package/blocks/crm/recipes/upsert-contact-with-company.ts +33 -0
- package/blocks/deals/README.md +78 -0
- package/blocks/deals/block.json +198 -0
- package/blocks/deals/components/deal-detail.tsx +134 -0
- package/blocks/deals/components/forecast-view.tsx +112 -0
- package/blocks/deals/components/pipeline-board.tsx +142 -0
- package/blocks/deals/components/stage-editor.tsx +96 -0
- package/blocks/deals/hooks/use-pipeline-deals.ts +67 -0
- package/blocks/deals/hooks/use-pipelines.ts +19 -0
- package/blocks/deals/lib/deals-types.ts +128 -0
- package/blocks/deals/recipes/01-create-deal-and-advance.ts +32 -0
- package/blocks/deals/recipes/02-pipeline-report.ts +35 -0
- package/blocks/deals/recipes/03-stale-deals-sweep.ts +20 -0
- package/blocks/dialer/README.md +114 -0
- package/blocks/dialer/block.json +260 -0
- package/blocks/dialer/components/call-log.tsx +78 -0
- package/blocks/dialer/components/call-queue.tsx +188 -0
- package/blocks/dialer/components/dialer-workspace.tsx +47 -0
- package/blocks/dialer/components/disposition-panel.tsx +75 -0
- package/blocks/dialer/components/live-calls-table.tsx +107 -0
- package/blocks/dialer/components/session-start-form.tsx +111 -0
- package/blocks/dialer/components/softphone.tsx +122 -0
- package/blocks/dialer/components/transcript-viewer.tsx +82 -0
- package/blocks/dialer/hooks/use-dialer-numbers.ts +19 -0
- package/blocks/dialer/hooks/use-dialer-session.ts +68 -0
- package/blocks/dialer/hooks/use-dispositions.ts +29 -0
- package/blocks/dialer/lib/dialer-types.ts +158 -0
- package/blocks/dialer/lib/softphone-transport.ts +67 -0
- package/blocks/dialer/lib/twilio-transport.ts +74 -0
- package/blocks/dialer/recipes/01-create-session-and-dial.ts +39 -0
- package/blocks/dialer/recipes/02-disposition-calls.ts +26 -0
- package/blocks/dialer/recipes/03-review-a-call.ts +23 -0
- package/blocks/enrichment/README.md +105 -0
- package/blocks/enrichment/block.json +204 -0
- package/blocks/enrichment/components/bulk-enrich-panel.tsx +205 -0
- package/blocks/enrichment/components/email-verify-badge.tsx +82 -0
- package/blocks/enrichment/components/enrich-button.tsx +136 -0
- package/blocks/enrichment/components/waterfall-config.tsx +127 -0
- package/blocks/enrichment/hooks/use-providers.ts +18 -0
- package/blocks/enrichment/lib/enrichment-types.ts +104 -0
- package/blocks/enrichment/recipes/bulk-enrich-and-poll.ts +41 -0
- package/blocks/enrichment/recipes/lookup-and-verify.ts +25 -0
- package/blocks/enrichment/recipes/waterfall-pipeline.ts +26 -0
- package/blocks/forms/README.md +112 -0
- package/blocks/forms/block.json +200 -0
- package/blocks/forms/components/form-builder.tsx +150 -0
- package/blocks/forms/components/form-embed.tsx +103 -0
- package/blocks/forms/components/form-mapping-editor.tsx +125 -0
- package/blocks/forms/components/site-audit-panel.tsx +73 -0
- package/blocks/forms/components/submissions-table.tsx +158 -0
- package/blocks/forms/lib/form-types.ts +127 -0
- package/blocks/forms/recipes/install-snippet-and-form.ts +21 -0
- package/blocks/forms/recipes/retry-failed-deliveries.ts +26 -0
- package/blocks/forms/recipes/route-form-to-list.ts +44 -0
- package/blocks/inbox/README.md +72 -0
- package/blocks/inbox/block.json +194 -0
- package/blocks/inbox/components/inbox-list.tsx +156 -0
- package/blocks/inbox/components/inbox-screen.tsx +49 -0
- package/blocks/inbox/components/reply-composer.tsx +183 -0
- package/blocks/inbox/components/tag-and-assign.tsx +189 -0
- package/blocks/inbox/components/thread-badge.tsx +17 -0
- package/blocks/inbox/components/thread-view.tsx +108 -0
- package/blocks/inbox/hooks/use-inbox-thread.ts +24 -0
- package/blocks/inbox/hooks/use-inbox-threads.ts +40 -0
- package/blocks/inbox/lib/idempotency.ts +30 -0
- package/blocks/inbox/lib/inbox-types.ts +100 -0
- package/blocks/inbox/recipes/01-triage-and-reply.ts +43 -0
- package/blocks/inbox/recipes/02-tag-and-assign.ts +31 -0
- package/blocks/inbox/recipes/03-hot-contacts-digest.ts +16 -0
- package/blocks/integrations/README.md +75 -0
- package/blocks/integrations/block.json +172 -0
- package/blocks/integrations/components/integration-list.tsx +165 -0
- package/blocks/integrations/components/sync-status.tsx +223 -0
- package/blocks/integrations/components/webhook-manager.tsx +300 -0
- package/blocks/integrations/lib/integrations-utils.ts +62 -0
- package/blocks/integrations/recipes/crm-sync-health.ts +19 -0
- package/blocks/integrations/recipes/push-contacts-to-crm.ts +20 -0
- package/blocks/integrations/recipes/register-webhook.ts +18 -0
- package/blocks/integrations/recipes/verify-webhook-route.ts +33 -0
- package/blocks/intent/README.md +57 -0
- package/blocks/intent/block.json +138 -0
- package/blocks/intent/components/account-intent-score.tsx +91 -0
- package/blocks/intent/components/intent-feed.tsx +215 -0
- package/blocks/intent/components/intent-keyword-manager.tsx +331 -0
- package/blocks/intent/hooks/use-account-intent.ts +46 -0
- package/blocks/intent/lib/intent-utils.ts +95 -0
- package/blocks/intent/recipes/rank-intent-accounts.ts +36 -0
- package/blocks/intent/recipes/track-competitor-domain.ts +23 -0
- package/blocks/intent/recipes/who-visited-url.ts +18 -0
- package/blocks/lifecycle/README.md +79 -0
- package/blocks/lifecycle/block.json +198 -0
- package/blocks/lifecycle/components/churn-list.tsx +92 -0
- package/blocks/lifecycle/components/customer-table.tsx +119 -0
- package/blocks/lifecycle/components/lead-queue.tsx +127 -0
- package/blocks/lifecycle/components/lifecycle-board.tsx +102 -0
- package/blocks/lifecycle/components/stage-move-dialog.tsx +75 -0
- package/blocks/lifecycle/hooks/use-debounced.ts +13 -0
- package/blocks/lifecycle/hooks/use-lifecycle-stages.ts +27 -0
- package/blocks/lifecycle/lib/lifecycle.ts +99 -0
- package/blocks/lifecycle/recipes/customer-brief.ts +23 -0
- package/blocks/lifecycle/recipes/move-company-stage.ts +17 -0
- package/blocks/lifecycle/recipes/read-lifecycle-board.ts +21 -0
- package/blocks/lifecycle/recipes/unlock-stage-contacts.ts +25 -0
- package/blocks/lists/README.md +96 -0
- package/blocks/lists/block.json +166 -0
- package/blocks/lists/components/add-to-list-button.tsx +111 -0
- package/blocks/lists/components/list-picker.tsx +94 -0
- package/blocks/lists/components/list-table.tsx +160 -0
- package/blocks/lists/components/suppression-manager.tsx +153 -0
- package/blocks/lists/recipes/create-list-and-add-contacts.ts +35 -0
- package/blocks/lists/recipes/refresh-and-check-lists.ts +21 -0
- package/blocks/lists/recipes/suppress-and-reinstate.ts +30 -0
- package/blocks/mailboxes/README.md +71 -0
- package/blocks/mailboxes/block.json +187 -0
- package/blocks/mailboxes/components/connect-mailbox.tsx +284 -0
- package/blocks/mailboxes/components/domain-purchase.tsx +271 -0
- package/blocks/mailboxes/components/mailbox-list.tsx +185 -0
- package/blocks/mailboxes/components/warmup-health.tsx +232 -0
- package/blocks/mailboxes/lib/mailboxes-utils.ts +69 -0
- package/blocks/mailboxes/recipes/buy-domain-and-mailboxes.ts +64 -0
- package/blocks/mailboxes/recipes/connect-imap-mailbox.ts +38 -0
- package/blocks/mailboxes/recipes/is-it-safe-to-send.ts +17 -0
- package/blocks/mailboxes/recipes/warmup-report.ts +31 -0
- package/blocks/newsletter/README.md +122 -0
- package/blocks/newsletter/block.json +224 -0
- package/blocks/newsletter/components/issue-form.tsx +124 -0
- package/blocks/newsletter/components/issue-scheduler.tsx +152 -0
- package/blocks/newsletter/components/newsletter-editor.tsx +131 -0
- package/blocks/newsletter/components/newsletter-stats.tsx +93 -0
- package/blocks/newsletter/components/subscriber-table.tsx +173 -0
- package/blocks/newsletter/lib/newsletter-types.ts +97 -0
- package/blocks/newsletter/recipes/create-newsletter-and-draft-issue.ts +27 -0
- package/blocks/newsletter/recipes/import-audience-and-send.ts +43 -0
- package/blocks/newsletter/recipes/issue-performance-report.ts +26 -0
- package/blocks/nurture/README.md +110 -0
- package/blocks/nurture/block.json +191 -0
- package/blocks/nurture/components/nurture-builder.tsx +114 -0
- package/blocks/nurture/components/nurture-enroll.tsx +107 -0
- package/blocks/nurture/components/nurture-form.tsx +120 -0
- package/blocks/nurture/components/nurture-stats.tsx +81 -0
- package/blocks/nurture/lib/nurture-types.ts +109 -0
- package/blocks/nurture/recipes/create-a-nurture.ts +34 -0
- package/blocks/nurture/recipes/enroll-contacts-and-launch.ts +24 -0
- package/blocks/nurture/recipes/nurture-health-check.ts +28 -0
- package/blocks/objects/README.md +120 -0
- package/blocks/objects/block.json +243 -0
- package/blocks/objects/components/attribute-field-input.tsx +82 -0
- package/blocks/objects/components/attribute-form-dialog.tsx +159 -0
- package/blocks/objects/components/object-not-found.tsx +34 -0
- package/blocks/objects/components/object-record-detail.tsx +143 -0
- package/blocks/objects/components/object-schema-editor.tsx +197 -0
- package/blocks/objects/components/object-table.tsx +130 -0
- package/blocks/objects/components/record-form-dialog.tsx +112 -0
- package/blocks/objects/components/record-roles-panel.tsx +106 -0
- package/blocks/objects/hooks/use-object-schema.ts +36 -0
- package/blocks/objects/lib/object-types.ts +214 -0
- package/blocks/objects/recipes/define-object-and-add-records.ts +24 -0
- package/blocks/objects/recipes/update-audit-and-archive-records.ts +39 -0
- package/blocks/objects/recipes/upsert-records-from-your-system.ts +30 -0
- package/blocks/pages/README.md +104 -0
- package/blocks/pages/block.json +223 -0
- package/blocks/pages/components/brand-kit-editor.tsx +152 -0
- package/blocks/pages/components/content-library.tsx +163 -0
- package/blocks/pages/components/page-editor.tsx +253 -0
- package/blocks/pages/components/page-list.tsx +159 -0
- package/blocks/pages/components/seo-report.tsx +159 -0
- package/blocks/pages/lib/studio.ts +77 -0
- package/blocks/pages/recipes/audit-site-seo.ts +28 -0
- package/blocks/pages/recipes/create-and-publish-page.ts +26 -0
- package/blocks/pages/recipes/edit-page-section.ts +25 -0
- package/blocks/pages/recipes/refresh-brand-kit.ts +22 -0
- package/blocks/quotes/README.md +79 -0
- package/blocks/quotes/block.json +183 -0
- package/blocks/quotes/components/product-picker.tsx +80 -0
- package/blocks/quotes/components/quote-builder.tsx +195 -0
- package/blocks/quotes/components/quote-list.tsx +79 -0
- package/blocks/quotes/components/quote-viewer.tsx +135 -0
- package/blocks/quotes/components/signature-status.tsx +77 -0
- package/blocks/quotes/hooks/use-debounced.ts +13 -0
- package/blocks/quotes/lib/quotes.ts +142 -0
- package/blocks/quotes/recipes/create-and-send-quote.ts +44 -0
- package/blocks/quotes/recipes/revise-sent-quote.ts +18 -0
- package/blocks/quotes/recipes/track-quote-signature.ts +22 -0
- package/blocks/radar/README.md +106 -0
- package/blocks/radar/block.json +189 -0
- package/blocks/radar/components/competitor-battle-card.tsx +53 -0
- package/blocks/radar/components/competitor-tracker.tsx +158 -0
- package/blocks/radar/components/radar-overview.tsx +67 -0
- package/blocks/radar/components/radar-report.tsx +86 -0
- package/blocks/radar/hooks/use-competitors.ts +23 -0
- package/blocks/radar/lib/radar-types.ts +124 -0
- package/blocks/radar/recipes/battle-card.ts +28 -0
- package/blocks/radar/recipes/deep-dive-report.ts +32 -0
- package/blocks/radar/recipes/marketing-snapshot.ts +18 -0
- package/blocks/radar/recipes/track-and-scan.ts +39 -0
- package/blocks/search/README.md +129 -0
- package/blocks/search/block.json +185 -0
- package/blocks/search/components/filter-builder.tsx +149 -0
- package/blocks/search/components/prospect-results.tsx +175 -0
- package/blocks/search/components/save-to-list-dialog.tsx +135 -0
- package/blocks/search/components/saved-searches.tsx +95 -0
- package/blocks/search/hooks/use-filter-options.ts +41 -0
- package/blocks/search/lib/search-types.ts +149 -0
- package/blocks/search/recipes/explore-filter-values.ts +27 -0
- package/blocks/search/recipes/saved-searches.ts +34 -0
- package/blocks/search/recipes/search-and-save-to-list.ts +37 -0
- package/blocks/sequencer/README.md +87 -0
- package/blocks/sequencer/block.json +214 -0
- package/blocks/sequencer/components/enroll-dialog.tsx +93 -0
- package/blocks/sequencer/components/sequence-builder.tsx +306 -0
- package/blocks/sequencer/components/sequence-list.tsx +200 -0
- package/blocks/sequencer/components/sequence-stats.tsx +256 -0
- package/blocks/sequencer/components/step-editor.tsx +213 -0
- package/blocks/sequencer/lib/sequencer-utils.ts +168 -0
- package/blocks/sequencer/recipes/create-multichannel-sequence.ts +51 -0
- package/blocks/sequencer/recipes/launch-and-enroll.ts +44 -0
- package/blocks/sequencer/recipes/pause-when-unsafe.ts +30 -0
- package/blocks/sequencer/recipes/sequence-report.ts +22 -0
- package/blocks/social-ads/README.md +128 -0
- package/blocks/social-ads/block.json +256 -0
- package/blocks/social-ads/components/ad-campaign-list.tsx +136 -0
- package/blocks/social-ads/components/audience-sync.tsx +150 -0
- package/blocks/social-ads/components/creative-gallery.tsx +121 -0
- package/blocks/social-ads/components/post-composer.tsx +122 -0
- package/blocks/social-ads/components/social-post-list.tsx +116 -0
- package/blocks/social-ads/hooks/use-social-accounts.ts +16 -0
- package/blocks/social-ads/lib/social-ads-types.ts +167 -0
- package/blocks/social-ads/recipes/creative-to-gallery.ts +41 -0
- package/blocks/social-ads/recipes/schedule-post.ts +51 -0
- package/blocks/social-ads/recipes/sync-audience-and-launch.ts +41 -0
- package/blocks/visitors/README.md +56 -0
- package/blocks/visitors/block.json +139 -0
- package/blocks/visitors/components/snippet-install.tsx +169 -0
- package/blocks/visitors/components/traffic-chart.tsx +156 -0
- package/blocks/visitors/components/visitor-company-card.tsx +92 -0
- package/blocks/visitors/components/visitor-feed.tsx +157 -0
- package/blocks/visitors/lib/visitors-utils.ts +78 -0
- package/blocks/visitors/recipes/check-snippet-installed.ts +24 -0
- package/blocks/visitors/recipes/recent-company-visits.ts +27 -0
- package/blocks/visitors/recipes/traffic-summary.ts +24 -0
- package/blocks/webchat/README.md +87 -0
- package/blocks/webchat/block.json +134 -0
- package/blocks/webchat/components/chat-widget-config.tsx +160 -0
- package/blocks/webchat/components/conversation-list.tsx +74 -0
- package/blocks/webchat/lib/webchat-types.ts +83 -0
- package/blocks/webchat/recipes/create-bot-and-style-widget.ts +24 -0
- package/blocks/webchat/recipes/list-chat-visitors.ts +24 -0
- package/blocks/workbench/README.md +135 -0
- package/blocks/workbench/block.json +259 -0
- package/blocks/workbench/components/column-recipe-editor.tsx +183 -0
- package/blocks/workbench/components/run-progress.tsx +132 -0
- package/blocks/workbench/components/workbench-add-rows.tsx +100 -0
- package/blocks/workbench/components/workbench-grid.tsx +159 -0
- package/blocks/workbench/components/workbench-row-actions.tsx +102 -0
- package/blocks/workbench/components/workbench-table-list.tsx +105 -0
- package/blocks/workbench/hooks/use-workbench-table.ts +30 -0
- package/blocks/workbench/lib/waterfall-presets.ts +119 -0
- package/blocks/workbench/lib/workbench-types.ts +191 -0
- package/blocks/workbench/recipes/build-enriched-table.ts +65 -0
- package/blocks/workbench/recipes/import-from-search.ts +42 -0
- package/blocks/workbench/recipes/scrape-into-table.ts +36 -0
- package/blocks/workbench/recipes/waterfall-column.ts +55 -0
- package/blocks/workflows/README.md +100 -0
- package/blocks/workflows/block.json +188 -0
- package/blocks/workflows/components/run-history.tsx +136 -0
- package/blocks/workflows/components/trigger-config.tsx +134 -0
- package/blocks/workflows/components/workflow-editor.tsx +142 -0
- package/blocks/workflows/components/workflow-list.tsx +153 -0
- package/blocks/workflows/hooks/use-workflow.ts +51 -0
- package/blocks/workflows/lib/graph.ts +187 -0
- package/blocks/workflows/recipes/create-workflow.ts +35 -0
- package/blocks/workflows/recipes/find-failed-runs.ts +15 -0
- package/blocks/workflows/recipes/run-workflow-and-wait.ts +32 -0
- package/blocks/workflows/recipes/set-trigger.ts +22 -0
- package/dist/cli.js +5330 -29
- package/dist/index.browser.js +7740 -0
- package/dist/index.browser.js.map +1 -0
- package/dist/index.browser.mjs +7717 -0
- package/dist/index.browser.mjs.map +1 -0
- package/dist/index.d.mts +1440 -21
- package/dist/index.d.ts +1440 -21
- package/dist/index.js +42 -3
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +42 -3
- package/dist/index.mjs.map +1 -1
- package/dist/react.browser.js +7598 -0
- package/dist/react.browser.js.map +1 -0
- package/dist/react.browser.mjs +7568 -0
- package/dist/react.browser.mjs.map +1 -0
- package/dist/react.d.mts +1340 -18
- package/dist/react.d.ts +1340 -18
- package/dist/react.js +41 -2
- package/dist/react.js.map +1 -1
- package/dist/react.mjs +41 -2
- package/dist/react.mjs.map +1 -1
- package/dist/types/agentSetup.d.ts +6 -6
- package/dist/types/blocks/add.d.ts +53 -0
- package/dist/types/blocks/create.d.ts +46 -0
- package/dist/types/blocks/doctor.d.ts +26 -0
- package/dist/types/blocks/registry.d.ts +134 -0
- package/dist/types/cli.d.ts +16 -0
- package/dist/types/core.d.ts +1 -1
- package/dist/types/crypto-browser.d.ts +2 -0
- package/dist/types/crypto-node.d.ts +6 -0
- package/dist/types/react.d.ts +1 -1
- package/dist/types/version.d.ts +2 -2
- package/dist/types/webhooks.d.ts +2 -2
- package/dist/ui/index.d.mts +54 -0
- package/dist/ui/index.d.ts +54 -0
- package/dist/ui/index.js +105 -0
- package/dist/ui/index.js.map +1 -0
- package/dist/ui/index.mjs +44 -0
- package/dist/ui/index.mjs.map +1 -0
- package/dist/ui/lib/compose-refs.d.mts +19 -0
- package/dist/ui/lib/compose-refs.d.ts +19 -0
- package/dist/ui/lib/compose-refs.js +76 -0
- package/dist/ui/lib/compose-refs.js.map +1 -0
- package/dist/ui/lib/compose-refs.mjs +41 -0
- package/dist/ui/lib/compose-refs.mjs.map +1 -0
- package/dist/ui/lib/use-mobile.d.mts +4 -0
- package/dist/ui/lib/use-mobile.d.ts +4 -0
- package/dist/ui/lib/use-mobile.js +57 -0
- package/dist/ui/lib/use-mobile.js.map +1 -0
- package/dist/ui/lib/use-mobile.mjs +22 -0
- package/dist/ui/lib/use-mobile.mjs.map +1 -0
- package/dist/ui/lib/utils.d.mts +10 -0
- package/dist/ui/lib/utils.d.ts +10 -0
- package/dist/ui/lib/utils.js +33 -0
- package/dist/ui/lib/utils.js.map +1 -0
- package/dist/ui/lib/utils.mjs +9 -0
- package/dist/ui/lib/utils.mjs.map +1 -0
- package/dist/ui/primitives/alert-dialog.d.mts +22 -0
- package/dist/ui/primitives/alert-dialog.d.ts +22 -0
- package/dist/ui/primitives/alert-dialog.js +163 -0
- package/dist/ui/primitives/alert-dialog.js.map +1 -0
- package/dist/ui/primitives/alert-dialog.mjs +119 -0
- package/dist/ui/primitives/alert-dialog.mjs.map +1 -0
- package/dist/ui/primitives/avatar.d.mts +8 -0
- package/dist/ui/primitives/avatar.d.ts +8 -0
- package/dist/ui/primitives/avatar.js +91 -0
- package/dist/ui/primitives/avatar.js.map +1 -0
- package/dist/ui/primitives/avatar.mjs +55 -0
- package/dist/ui/primitives/avatar.mjs.map +1 -0
- package/dist/ui/primitives/breadcrumb.d.mts +13 -0
- package/dist/ui/primitives/breadcrumb.d.ts +13 -0
- package/dist/ui/primitives/breadcrumb.js +127 -0
- package/dist/ui/primitives/breadcrumb.js.map +1 -0
- package/dist/ui/primitives/breadcrumb.mjs +97 -0
- package/dist/ui/primitives/breadcrumb.mjs.map +1 -0
- package/dist/ui/primitives/button.d.mts +15 -0
- package/dist/ui/primitives/button.d.ts +15 -0
- package/dist/ui/primitives/button.js +91 -0
- package/dist/ui/primitives/button.js.map +1 -0
- package/dist/ui/primitives/button.mjs +56 -0
- package/dist/ui/primitives/button.mjs.map +1 -0
- package/dist/ui/primitives/card.d.mts +10 -0
- package/dist/ui/primitives/card.d.ts +10 -0
- package/dist/ui/primitives/card.js +101 -0
- package/dist/ui/primitives/card.js.map +1 -0
- package/dist/ui/primitives/card.mjs +62 -0
- package/dist/ui/primitives/card.mjs.map +1 -0
- package/dist/ui/primitives/carousel.d.mts +45 -0
- package/dist/ui/primitives/carousel.d.ts +45 -0
- package/dist/ui/primitives/carousel.js +300 -0
- package/dist/ui/primitives/carousel.js.map +1 -0
- package/dist/ui/primitives/carousel.mjs +261 -0
- package/dist/ui/primitives/carousel.mjs.map +1 -0
- package/dist/ui/primitives/chart.d.mts +68 -0
- package/dist/ui/primitives/chart.d.ts +68 -0
- package/dist/ui/primitives/chart.js +280 -0
- package/dist/ui/primitives/chart.js.map +1 -0
- package/dist/ui/primitives/chart.mjs +241 -0
- package/dist/ui/primitives/chart.mjs.map +1 -0
- package/dist/ui/primitives/collapsible.d.mts +8 -0
- package/dist/ui/primitives/collapsible.d.ts +8 -0
- package/dist/ui/primitives/collapsible.js +72 -0
- package/dist/ui/primitives/collapsible.js.map +1 -0
- package/dist/ui/primitives/collapsible.mjs +36 -0
- package/dist/ui/primitives/collapsible.mjs.map +1 -0
- package/dist/ui/primitives/context-menu.d.mts +29 -0
- package/dist/ui/primitives/context-menu.d.ts +29 -0
- package/dist/ui/primitives/context-menu.js +203 -0
- package/dist/ui/primitives/context-menu.js.map +1 -0
- package/dist/ui/primitives/context-menu.mjs +155 -0
- package/dist/ui/primitives/context-menu.mjs.map +1 -0
- package/dist/ui/primitives/dialog.d.mts +26 -0
- package/dist/ui/primitives/dialog.d.ts +26 -0
- package/dist/ui/primitives/dialog.js +192 -0
- package/dist/ui/primitives/dialog.js.map +1 -0
- package/dist/ui/primitives/dialog.mjs +148 -0
- package/dist/ui/primitives/dialog.mjs.map +1 -0
- package/dist/ui/primitives/drawer.d.mts +26 -0
- package/dist/ui/primitives/drawer.d.ts +26 -0
- package/dist/ui/primitives/drawer.js +146 -0
- package/dist/ui/primitives/drawer.js.map +1 -0
- package/dist/ui/primitives/drawer.mjs +103 -0
- package/dist/ui/primitives/drawer.mjs.map +1 -0
- package/dist/ui/primitives/input.d.mts +5 -0
- package/dist/ui/primitives/input.d.ts +5 -0
- package/dist/ui/primitives/input.js +46 -0
- package/dist/ui/primitives/input.js.map +1 -0
- package/dist/ui/primitives/input.mjs +22 -0
- package/dist/ui/primitives/input.mjs.map +1 -0
- package/dist/ui/primitives/item.d.mts +27 -0
- package/dist/ui/primitives/item.d.ts +27 -0
- package/dist/ui/primitives/item.js +221 -0
- package/dist/ui/primitives/item.js.map +1 -0
- package/dist/ui/primitives/item.mjs +188 -0
- package/dist/ui/primitives/item.mjs.map +1 -0
- package/dist/ui/primitives/kbd.d.mts +15 -0
- package/dist/ui/primitives/kbd.d.ts +15 -0
- package/dist/ui/primitives/kbd.js +45 -0
- package/dist/ui/primitives/kbd.js.map +1 -0
- package/dist/ui/primitives/kbd.mjs +21 -0
- package/dist/ui/primitives/kbd.mjs.map +1 -0
- package/dist/ui/primitives/label.d.mts +6 -0
- package/dist/ui/primitives/label.d.ts +6 -0
- package/dist/ui/primitives/label.js +55 -0
- package/dist/ui/primitives/label.js.map +1 -0
- package/dist/ui/primitives/label.mjs +21 -0
- package/dist/ui/primitives/label.mjs.map +1 -0
- package/dist/ui/primitives/resizable.d.mts +10 -0
- package/dist/ui/primitives/resizable.d.ts +10 -0
- package/dist/ui/primitives/resizable.js +68 -0
- package/dist/ui/primitives/resizable.js.map +1 -0
- package/dist/ui/primitives/resizable.mjs +42 -0
- package/dist/ui/primitives/resizable.mjs.map +1 -0
- package/dist/ui/primitives/save-status.d.mts +31 -0
- package/dist/ui/primitives/save-status.d.ts +31 -0
- package/dist/ui/primitives/save-status.js +150 -0
- package/dist/ui/primitives/save-status.js.map +1 -0
- package/dist/ui/primitives/save-status.mjs +126 -0
- package/dist/ui/primitives/save-status.mjs.map +1 -0
- package/dist/ui/primitives/separator.d.mts +6 -0
- package/dist/ui/primitives/separator.d.ts +6 -0
- package/dist/ui/primitives/separator.js +62 -0
- package/dist/ui/primitives/separator.js.map +1 -0
- package/dist/ui/primitives/separator.mjs +28 -0
- package/dist/ui/primitives/separator.mjs.map +1 -0
- package/dist/ui/primitives/sheet.d.mts +16 -0
- package/dist/ui/primitives/sheet.d.ts +16 -0
- package/dist/ui/primitives/sheet.js +170 -0
- package/dist/ui/primitives/sheet.js.map +1 -0
- package/dist/ui/primitives/sheet.mjs +129 -0
- package/dist/ui/primitives/sheet.mjs.map +1 -0
- package/dist/ui/primitives/sidebar.d.mts +74 -0
- package/dist/ui/primitives/sidebar.d.ts +74 -0
- package/dist/ui/primitives/sidebar.js +697 -0
- package/dist/ui/primitives/sidebar.js.map +1 -0
- package/dist/ui/primitives/sidebar.mjs +651 -0
- package/dist/ui/primitives/sidebar.mjs.map +1 -0
- package/dist/ui/primitives/skeleton.d.mts +5 -0
- package/dist/ui/primitives/skeleton.d.ts +5 -0
- package/dist/ui/primitives/skeleton.js +40 -0
- package/dist/ui/primitives/skeleton.js.map +1 -0
- package/dist/ui/primitives/skeleton.mjs +16 -0
- package/dist/ui/primitives/skeleton.mjs.map +1 -0
- package/dist/ui/primitives/slider.d.mts +20 -0
- package/dist/ui/primitives/slider.d.ts +20 -0
- package/dist/ui/primitives/slider.js +112 -0
- package/dist/ui/primitives/slider.js.map +1 -0
- package/dist/ui/primitives/slider.mjs +78 -0
- package/dist/ui/primitives/slider.mjs.map +1 -0
- package/dist/ui/primitives/sortable.d.mts +50 -0
- package/dist/ui/primitives/sortable.d.ts +50 -0
- package/dist/ui/primitives/sortable.js +456 -0
- package/dist/ui/primitives/sortable.js.map +1 -0
- package/dist/ui/primitives/sortable.mjs +435 -0
- package/dist/ui/primitives/sortable.mjs.map +1 -0
- package/dist/ui/primitives/switch.d.mts +6 -0
- package/dist/ui/primitives/switch.d.ts +6 -0
- package/dist/ui/primitives/switch.js +67 -0
- package/dist/ui/primitives/switch.js.map +1 -0
- package/dist/ui/primitives/switch.mjs +33 -0
- package/dist/ui/primitives/switch.mjs.map +1 -0
- package/dist/ui/primitives/table.d.mts +12 -0
- package/dist/ui/primitives/table.d.ts +12 -0
- package/dist/ui/primitives/table.js +130 -0
- package/dist/ui/primitives/table.js.map +1 -0
- package/dist/ui/primitives/table.mjs +89 -0
- package/dist/ui/primitives/table.mjs.map +1 -0
- package/dist/ui/primitives/tabs.d.mts +9 -0
- package/dist/ui/primitives/tabs.d.ts +9 -0
- package/dist/ui/primitives/tabs.js +86 -0
- package/dist/ui/primitives/tabs.js.map +1 -0
- package/dist/ui/primitives/tabs.mjs +49 -0
- package/dist/ui/primitives/tabs.mjs.map +1 -0
- package/dist/ui/primitives/textarea.d.mts +5 -0
- package/dist/ui/primitives/textarea.d.ts +5 -0
- package/dist/ui/primitives/textarea.js +43 -0
- package/dist/ui/primitives/textarea.js.map +1 -0
- package/dist/ui/primitives/textarea.mjs +19 -0
- package/dist/ui/primitives/textarea.mjs.map +1 -0
- package/dist/ui/primitives/tooltip.d.mts +9 -0
- package/dist/ui/primitives/tooltip.d.ts +9 -0
- package/dist/ui/primitives/tooltip.js +94 -0
- package/dist/ui/primitives/tooltip.js.map +1 -0
- package/dist/ui/primitives/tooltip.mjs +57 -0
- package/dist/ui/primitives/tooltip.mjs.map +1 -0
- package/docs/agents.md +72 -29
- package/docs/api/README.md +6 -3
- package/docs/api/appObjects.d.ts +1 -1
- package/docs/api/appObjects.md +1 -1
- package/docs/api/by-path.md +29 -0
- package/docs/api/objects.d.ts +72 -5
- package/docs/api/objects.md +84 -7
- package/docs/api/processLists.d.ts +247 -0
- package/docs/api/processLists.md +409 -0
- package/docs/api/reports.d.ts +476 -0
- package/docs/api/reports.md +539 -0
- package/docs/api/scheduleBuy.d.ts +12 -8
- package/docs/api/scheduleBuy.md +13 -10
- package/docs/api/seo.d.ts +406 -3
- package/docs/api/seo.md +505 -1
- package/docs/api/workspaceBackup.d.ts +324 -0
- package/docs/api/workspaceBackup.md +427 -0
- package/package.json +174 -5
- package/stacks/6sense.json +46 -0
- package/stacks/README.md +20 -0
- package/stacks/apollo.json +128 -0
- package/stacks/clay.json +59 -0
- package/stacks/gong.json +72 -0
- package/stacks/hubspot.json +162 -0
- package/stacks/intercom.json +53 -0
- package/stacks/mailchimp.json +71 -0
- package/stacks/outreach.json +86 -0
- package/stacks/salesforce.json +94 -0
- package/stacks/stack.schema.json +32 -0
- package/stacks/starter.json +22 -0
- package/styles/styles.css +2 -0
- package/styles/theme.css +181 -0
- package/styles/tokens.css +284 -0
- package/templates/next-app/README.md.tmpl +35 -0
- package/templates/next-app/_pages/redirect-page.tsx.tmpl +5 -0
- package/templates/next-app/_pages/route-page.tsx.tmpl +9 -0
- package/templates/next-app/app/globals.css.tmpl +8 -0
- package/templates/next-app/app/layout.tsx.tmpl +19 -0
- package/templates/next-app/components/app-shell.tsx.tmpl +34 -0
- package/templates/next-app/dot-env.local.example.tmpl +6 -0
- package/templates/next-app/dot-gitignore.tmpl +7 -0
- package/templates/next-app/lib/nav.ts.tmpl +2 -0
- package/templates/next-app/next.config.mjs.tmpl +4 -0
- package/templates/next-app/package.json.tmpl +25 -0
- package/templates/next-app/postcss.config.mjs.tmpl +4 -0
- package/templates/next-app/tsconfig.json.tmpl +21 -0
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @title Sync a list to an ad platform and launch a campaign on it
|
|
3
|
+
* @when Retargeting or lookalike campaigns from a graph8 list. The ad account must already be connected in graph8
|
|
4
|
+
* (OAuth happens in the app only; the API cannot do it). Both steps are EXTERNAL: creating a sync sends the list's
|
|
5
|
+
* contacts to the platform, launching creates a campaign that spends from the ad account. Get a human to approve the
|
|
6
|
+
* list (and its size), the platform, the daily budget and the dates. Start and end are date-only: pass "YYYY-MM-DD".
|
|
7
|
+
* A 409 on create means that list is already synced to that platform: reuse the existing sync.
|
|
8
|
+
*/
|
|
9
|
+
import { g8 } from "@graph8/sdk";
|
|
10
|
+
|
|
11
|
+
g8.init({ apiKey: process.env.G8_API_KEY! });
|
|
12
|
+
|
|
13
|
+
export async function syncList(audienceId: number, platform: "meta" | "linkedin" | "google" | "x") {
|
|
14
|
+
const res = await g8.api.audienceSyncs.createAudienceSync({ body: { audience_id: audienceId, platform, mode: "mirror", refresh_cadence_hours: 24 } });
|
|
15
|
+
return (res.data as { id: number }).id;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export async function pushNow(syncId: number) {
|
|
19
|
+
return g8.api.audienceSyncs.triggerAudienceSync({ path: { config_id: syncId } });
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* HTTP 200 does NOT mean launched: a refused launch answers 200 with `status: "error"` and a `message`. Check it.
|
|
24
|
+
* `currency` defaults to USD; Google needs `creative_image_url`.
|
|
25
|
+
*/
|
|
26
|
+
export async function launch(syncId: number, name: string, dailyBudget: number, destinationUrl: string, startDate: string, endDate?: string, currency = "USD") {
|
|
27
|
+
const res = await g8.api.adCampaigns.launchAdCampaign({
|
|
28
|
+
path: { config_id: syncId },
|
|
29
|
+
body: { name, daily_budget: dailyBudget, currency, destination_url: destinationUrl, start_date: startDate, ...(endDate ? { end_date: endDate } : {}) },
|
|
30
|
+
});
|
|
31
|
+
const body = res.data as { status?: string; message?: string };
|
|
32
|
+
if (body.status === "error" || body.status === "failed") throw new Error(body.message ?? "The platform refused the launch");
|
|
33
|
+
return res.data;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** Results by campaign for the last `days`; join on `platform_campaign_id` (one sync can carry several campaigns, so `sync_config_id` alone would sum them). Spend is in each campaign's currency. */
|
|
37
|
+
export async function results(days = 30) {
|
|
38
|
+
const [campaigns, metrics] = await Promise.all([g8.api.adCampaigns.listAdCampaigns(), g8.api.adCampaigns.getPerCampaignMetrics({ query: { days } })]);
|
|
39
|
+
const rows = ((metrics.data as { metrics?: Array<{ sync_config_id: number; platform_campaign_id?: string; impressions: number; clicks: number; spend: number }> }).metrics) ?? [];
|
|
40
|
+
return campaigns.data.map((c) => ({ id: c.id, name: c.name, status: c.status, metrics: rows.filter((m) => m.sync_config_id === c.sync_config_id && (!c.platform_campaign_id || m.platform_campaign_id === c.platform_campaign_id)) }));
|
|
41
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# visitors
|
|
2
|
+
|
|
3
|
+
See which companies and people visit your website, install the graph8 tracking snippet and prove it works,
|
|
4
|
+
and chart traffic. The Apollo / 6sense "website visitors" screen.
|
|
5
|
+
|
|
6
|
+
Depends on: `crm`. Install: `npx @graph8/sdk add visitors`.
|
|
7
|
+
|
|
8
|
+
## Components
|
|
9
|
+
|
|
10
|
+
| Component | What it does | Tiers |
|
|
11
|
+
|---|---|---|
|
|
12
|
+
| `VisitorFeed` | Polled feed of company visits (default) and identified people. Paging, refresh, bots excluded. | read |
|
|
13
|
+
| `VisitorCompanyCard` | One visiting company with its page views; "Full journey" loads every page view. | read |
|
|
14
|
+
| `SnippetInstall` | Streams picker, React and script-tag snippets with copy, and an install check that polls until events arrive. | read |
|
|
15
|
+
| `TrafficChart` | Visitors, sessions or page views per day (inline SVG), top pages, "N on the site now". | read |
|
|
16
|
+
|
|
17
|
+
Everything here is read tier: no confirmation dialog is needed. No component spends credits or contacts anyone.
|
|
18
|
+
|
|
19
|
+
## The flow, in order
|
|
20
|
+
|
|
21
|
+
1. **Get the snippet.** `SnippetInstall` -> `listOrgStreams` (the websites) -> `getOrgSnippet` with the chosen
|
|
22
|
+
`stream_id`. The `write_key` it returns is PUBLIC and goes in page source.
|
|
23
|
+
2. **The human installs it** (paste into the site or the Next.js layout). This is the one step the agent cannot do.
|
|
24
|
+
3. **Prove it.** "Check installation" polls `checkTrackingInstallation` with that `write_key` until
|
|
25
|
+
`status` is `receiving` (also `no_events`, `unknown`). It backs off and gives up after about 5 minutes.
|
|
26
|
+
4. **Show visitors.** `VisitorFeed` -> `listCompanyVisitorEvents` (`show_mode=grouped`) and, on the People tab,
|
|
27
|
+
`listContactVisitorEvents`. Each grouped row's `COMPANY_ID` feeds the card's journey call
|
|
28
|
+
(`filter={"company_id": id}`, `show_mode=raw`).
|
|
29
|
+
5. **Chart traffic.** `TrafficChart` -> `getAudienceTrend`, `getTopPages`, `getRealtime` (re-read every 10 s).
|
|
30
|
+
|
|
31
|
+
## Prerequisites
|
|
32
|
+
|
|
33
|
+
- Tracking snippet on the site (`where: "app"`). Verify: Check installation reports events received.
|
|
34
|
+
- A website stream exists in the workspace (`where: "api"`): `GET /streams` is non-empty.
|
|
35
|
+
- The People tab joins visits to contacts, so the API key also needs the contacts read scope.
|
|
36
|
+
|
|
37
|
+
## Defaults (do not ask the human)
|
|
38
|
+
|
|
39
|
+
- Poll the feed every 30 s; realtime every 10 s (there are no streams or sockets).
|
|
40
|
+
- Companies tab first; bots and internal traffic excluded; 25 rows per page.
|
|
41
|
+
- The org's main stream unless the person picks another.
|
|
42
|
+
|
|
43
|
+
## Operations
|
|
44
|
+
|
|
45
|
+
| Operation | Method and path | Tier | Used by |
|
|
46
|
+
|---|---|---|---|
|
|
47
|
+
| `list_company_visitor_events_visitors_companies_events_get` | GET /visitors/companies/events | read | VisitorFeed, VisitorCompanyCard |
|
|
48
|
+
| `list_contact_visitor_events_visitors_contacts_events_get` | GET /visitors/contacts/events | read | VisitorFeed |
|
|
49
|
+
| `list_org_streams_streams_get` | GET /streams | read | SnippetInstall |
|
|
50
|
+
| `get_org_snippet_snippet_get` | GET /snippet | read | SnippetInstall |
|
|
51
|
+
| `check_tracking_installation_visitors_installation_check_get` | GET /visitors/installation-check | read | SnippetInstall |
|
|
52
|
+
| `get_audience_trend_analytics_audience_trend_get` | GET /analytics/audience/trend | read | TrafficChart |
|
|
53
|
+
| `get_realtime_analytics_realtime_get` | GET /analytics/realtime | read | TrafficChart |
|
|
54
|
+
| `get_top_pages_analytics_behavior_pages_get` | GET /analytics/behavior/pages | read | TrafficChart |
|
|
55
|
+
|
|
56
|
+
Recipes (`recipes/`): `check-snippet-installed.ts`, `recent-company-visits.ts`, `traffic-summary.ts`.
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "../block.schema.json",
|
|
3
|
+
"id": "visitors",
|
|
4
|
+
"title": "Website visitors",
|
|
5
|
+
"summary": "See which companies and people visit your site, install the tracking snippet, and chart traffic.",
|
|
6
|
+
"features": [
|
|
7
|
+
"live-ish company and people visitor feed",
|
|
8
|
+
"company visit journey",
|
|
9
|
+
"tracking snippet with install check",
|
|
10
|
+
"traffic trend, realtime and top pages"
|
|
11
|
+
],
|
|
12
|
+
"dependsOn": [
|
|
13
|
+
"crm"
|
|
14
|
+
],
|
|
15
|
+
"operations": [
|
|
16
|
+
"list_company_visitor_events_visitors_companies_events_get",
|
|
17
|
+
"list_contact_visitor_events_visitors_contacts_events_get",
|
|
18
|
+
"list_org_streams_streams_get",
|
|
19
|
+
"get_org_snippet_snippet_get",
|
|
20
|
+
"check_tracking_installation_visitors_installation_check_get",
|
|
21
|
+
"get_audience_trend_analytics_audience_trend_get",
|
|
22
|
+
"get_realtime_analytics_realtime_get",
|
|
23
|
+
"get_top_pages_analytics_behavior_pages_get"
|
|
24
|
+
],
|
|
25
|
+
"components": [
|
|
26
|
+
{
|
|
27
|
+
"name": "VisitorFeed",
|
|
28
|
+
"file": "components/visitor-feed.tsx",
|
|
29
|
+
"purpose": "Polled feed of company and identified-person visits with paging and refresh.",
|
|
30
|
+
"operations": [
|
|
31
|
+
"list_company_visitor_events_visitors_companies_events_get",
|
|
32
|
+
"list_contact_visitor_events_visitors_contacts_events_get"
|
|
33
|
+
],
|
|
34
|
+
"tiers": [
|
|
35
|
+
"read"
|
|
36
|
+
]
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
"name": "VisitorCompanyCard",
|
|
40
|
+
"file": "components/visitor-company-card.tsx",
|
|
41
|
+
"purpose": "One visiting company with its page views and an on-demand full journey.",
|
|
42
|
+
"operations": [
|
|
43
|
+
"list_company_visitor_events_visitors_companies_events_get"
|
|
44
|
+
],
|
|
45
|
+
"tiers": [
|
|
46
|
+
"read"
|
|
47
|
+
]
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
"name": "SnippetInstall",
|
|
51
|
+
"file": "components/snippet-install.tsx",
|
|
52
|
+
"purpose": "Show the React and script-tag snippets for a website stream and poll until events arrive.",
|
|
53
|
+
"operations": [
|
|
54
|
+
"list_org_streams_streams_get",
|
|
55
|
+
"get_org_snippet_snippet_get",
|
|
56
|
+
"check_tracking_installation_visitors_installation_check_get"
|
|
57
|
+
],
|
|
58
|
+
"tiers": [
|
|
59
|
+
"read"
|
|
60
|
+
]
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
"name": "TrafficChart",
|
|
64
|
+
"file": "components/traffic-chart.tsx",
|
|
65
|
+
"purpose": "Daily visitors, sessions and page views as an SVG line chart, with top pages and a realtime counter.",
|
|
66
|
+
"operations": [
|
|
67
|
+
"get_audience_trend_analytics_audience_trend_get",
|
|
68
|
+
"get_realtime_analytics_realtime_get",
|
|
69
|
+
"get_top_pages_analytics_behavior_pages_get"
|
|
70
|
+
],
|
|
71
|
+
"tiers": [
|
|
72
|
+
"read"
|
|
73
|
+
]
|
|
74
|
+
}
|
|
75
|
+
],
|
|
76
|
+
"hooks": [],
|
|
77
|
+
"lib": [
|
|
78
|
+
{
|
|
79
|
+
"file": "lib/visitors-utils.ts",
|
|
80
|
+
"purpose": "Visitor event types, label and time helpers, and envelope readers shared by the components."
|
|
81
|
+
}
|
|
82
|
+
],
|
|
83
|
+
"recipes": [
|
|
84
|
+
{
|
|
85
|
+
"file": "recipes/check-snippet-installed.ts",
|
|
86
|
+
"title": "Fetch the tracking snippet and confirm events are arriving",
|
|
87
|
+
"when": "Onboarding a website."
|
|
88
|
+
},
|
|
89
|
+
{
|
|
90
|
+
"file": "recipes/recent-company-visits.ts",
|
|
91
|
+
"title": "List the companies that visited your site recently",
|
|
92
|
+
"when": "Building a who-is-on-our-site view or a daily digest."
|
|
93
|
+
},
|
|
94
|
+
{
|
|
95
|
+
"file": "recipes/traffic-summary.ts",
|
|
96
|
+
"title": "Summarise site traffic for the last N days",
|
|
97
|
+
"when": "A weekly report or dashboard header."
|
|
98
|
+
}
|
|
99
|
+
],
|
|
100
|
+
"prerequisites": [
|
|
101
|
+
{
|
|
102
|
+
"id": "snippet-on-site",
|
|
103
|
+
"description": "The graph8 tracking snippet is pasted into the website the visitors feed should watch.",
|
|
104
|
+
"where": "app",
|
|
105
|
+
"verify": "SnippetInstall's Check installation reports events received (GET /visitors/installation-check status is receiving)."
|
|
106
|
+
},
|
|
107
|
+
{
|
|
108
|
+
"id": "website-stream",
|
|
109
|
+
"description": "The workspace has at least one website stream (it gives the write key).",
|
|
110
|
+
"where": "api",
|
|
111
|
+
"verify": "GET /streams returns at least one stream."
|
|
112
|
+
}
|
|
113
|
+
],
|
|
114
|
+
"scopes": [
|
|
115
|
+
"analytics:read",
|
|
116
|
+
"integrations:read",
|
|
117
|
+
"visitors:read"
|
|
118
|
+
],
|
|
119
|
+
"defaults": [
|
|
120
|
+
{
|
|
121
|
+
"decision": "Visitor feed refresh",
|
|
122
|
+
"default": "Poll every 30 seconds (no streams exist)",
|
|
123
|
+
"askHuman": false
|
|
124
|
+
},
|
|
125
|
+
{
|
|
126
|
+
"decision": "Bots and internal traffic",
|
|
127
|
+
"default": "Excluded",
|
|
128
|
+
"askHuman": false
|
|
129
|
+
},
|
|
130
|
+
{
|
|
131
|
+
"decision": "Which website stream to show",
|
|
132
|
+
"default": "The org's main stream",
|
|
133
|
+
"askHuman": false
|
|
134
|
+
}
|
|
135
|
+
],
|
|
136
|
+
"peerDependencies": {
|
|
137
|
+
"react": ">=18"
|
|
138
|
+
}
|
|
139
|
+
}
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* SnippetInstall: shows the tracking snippet for a website stream, then watches for the first events so
|
|
5
|
+
* the person knows the install worked.
|
|
6
|
+
*
|
|
7
|
+
* SDK operations, in order:
|
|
8
|
+
* 1. listOrgStreams GET /streams (read; the org's websites and their write keys)
|
|
9
|
+
* 2. getOrgSnippet GET /snippet?stream_id= (read; write key + React and script-tag embeds)
|
|
10
|
+
* 3. checkTrackingInstallation GET /visitors/installation-check (read; polled until events arrive)
|
|
11
|
+
*
|
|
12
|
+
* Tiers: all read; no confirmation. The write key is the PUBLIC browser key, safe to ship in a page.
|
|
13
|
+
* Step 3 is a poll with backoff (there is no push): it stops at "receiving", or after about 5 minutes.
|
|
14
|
+
* The person must paste the snippet into their site (a `where: "app"` prerequisite) before step 3 can pass.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { useState } from "react";
|
|
18
|
+
import { Button, Skeleton } from "@graph8/sdk/ui";
|
|
19
|
+
import { g8Get } from "@/g8/lib/g8-client";
|
|
20
|
+
import { useG8Query } from "@/g8/hooks/use-g8-query";
|
|
21
|
+
import { useJobPoll } from "@/g8/hooks/use-job-poll";
|
|
22
|
+
import { ApiError } from "@/g8/components/api-error";
|
|
23
|
+
import { EmptyState } from "@/g8/components/empty-state";
|
|
24
|
+
|
|
25
|
+
export interface SnippetInstallProps {
|
|
26
|
+
/** Pre-select a stream id (from listOrgStreams). Default: the org's main stream. */
|
|
27
|
+
streamId?: string;
|
|
28
|
+
/** Called once events are seen. */
|
|
29
|
+
onInstalled?: () => void;
|
|
30
|
+
className?: string;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
interface Stream {
|
|
34
|
+
id: string;
|
|
35
|
+
name: string;
|
|
36
|
+
domains?: string[];
|
|
37
|
+
is_default?: boolean;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
interface SnippetPayload {
|
|
41
|
+
write_key: string;
|
|
42
|
+
react_snippet: string;
|
|
43
|
+
script_tag: string;
|
|
44
|
+
stream_id?: string | null;
|
|
45
|
+
stream_name?: string | null;
|
|
46
|
+
warning?: string | null;
|
|
47
|
+
action_required?: boolean;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
interface InstallStatus {
|
|
51
|
+
status: "receiving" | "no_events" | "unknown";
|
|
52
|
+
event_count: number;
|
|
53
|
+
last_event_at: string | null;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function CodeBlock({ label, code }: { label: string; code: string }) {
|
|
57
|
+
const [copied, setCopied] = useState(false);
|
|
58
|
+
const copy = async () => {
|
|
59
|
+
try {
|
|
60
|
+
await navigator.clipboard.writeText(code);
|
|
61
|
+
setCopied(true);
|
|
62
|
+
} catch {
|
|
63
|
+
setCopied(false);
|
|
64
|
+
}
|
|
65
|
+
};
|
|
66
|
+
return (
|
|
67
|
+
<div className="space-y-1">
|
|
68
|
+
<div className="flex items-center justify-between">
|
|
69
|
+
<span className="text-sm font-medium">{label}</span>
|
|
70
|
+
<Button size="sm" variant="outline" onClick={() => void copy()}>
|
|
71
|
+
{copied ? "Copied" : "Copy"}
|
|
72
|
+
</Button>
|
|
73
|
+
</div>
|
|
74
|
+
<pre className="overflow-x-auto rounded bg-gray-950 p-3 text-xs text-gray-100">
|
|
75
|
+
<code>{code}</code>
|
|
76
|
+
</pre>
|
|
77
|
+
</div>
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function InstallCheck({ writeKey, onInstalled }: { writeKey: string; onInstalled?: () => void }) {
|
|
82
|
+
const check = useJobPoll<InstallStatus>(
|
|
83
|
+
async (signal) => (await g8Get<InstallStatus>("/visitors/installation-check", { write_key: writeKey }, { signal })).data,
|
|
84
|
+
{
|
|
85
|
+
isTerminal: (s) => s.status === "receiving" || s.status === "unknown",
|
|
86
|
+
enabled: false,
|
|
87
|
+
intervalMs: 5000,
|
|
88
|
+
maxIntervalMs: 15000,
|
|
89
|
+
maxAttempts: 25,
|
|
90
|
+
onDone: (s) => s.status === "receiving" && onInstalled?.(),
|
|
91
|
+
},
|
|
92
|
+
);
|
|
93
|
+
const s = check.status;
|
|
94
|
+
return (
|
|
95
|
+
<div className="space-y-2" aria-live="polite">
|
|
96
|
+
<Button onClick={check.start} disabled={check.polling}>
|
|
97
|
+
{check.polling ? "Waiting for the first visit..." : "Check installation"}
|
|
98
|
+
</Button>
|
|
99
|
+
{check.error ? <ApiError error={check.error} /> : null}
|
|
100
|
+
{s?.status === "receiving" ? (
|
|
101
|
+
<p role="status" className="text-sm text-green-700">
|
|
102
|
+
Installed: {s.event_count.toLocaleString()} events received.
|
|
103
|
+
</p>
|
|
104
|
+
) : null}
|
|
105
|
+
{s?.status === "unknown" ? (
|
|
106
|
+
<p role="status" className="text-sm text-amber-700">
|
|
107
|
+
Your workspace cannot be probed yet. Open your site once and check the Visitors feed.
|
|
108
|
+
</p>
|
|
109
|
+
) : null}
|
|
110
|
+
{s?.status === "no_events" && !check.polling ? (
|
|
111
|
+
<p role="status" className="text-sm text-gray-600">
|
|
112
|
+
No events yet. Load a page that has the snippet, then check again.
|
|
113
|
+
</p>
|
|
114
|
+
) : null}
|
|
115
|
+
{s?.status === "no_events" && check.polling ? (
|
|
116
|
+
<p className="text-sm text-gray-600">No events yet. Open a page that has the snippet.</p>
|
|
117
|
+
) : null}
|
|
118
|
+
</div>
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export function SnippetInstall({ streamId, onInstalled, className }: SnippetInstallProps) {
|
|
123
|
+
const [selected, setSelected] = useState<string | undefined>(streamId);
|
|
124
|
+
const streams = useG8Query((signal) => g8Get<Stream[]>("/streams", { limit: 50 }, { signal }), []);
|
|
125
|
+
const snippet = useG8Query((signal) => g8Get<SnippetPayload>("/snippet", { stream_id: selected }, { signal }), [selected]);
|
|
126
|
+
const list = streams.data?.data ?? [];
|
|
127
|
+
|
|
128
|
+
if (snippet.error) return <ApiError error={snippet.error} onRetry={() => void snippet.refetch()} className={className} />;
|
|
129
|
+
if (snippet.loading && !snippet.data) return <Skeleton className={`h-40 w-full ${className ?? ""}`} />;
|
|
130
|
+
const s = snippet.data?.data;
|
|
131
|
+
if (!s)
|
|
132
|
+
return (
|
|
133
|
+
<EmptyState
|
|
134
|
+
title="No tracking stream yet"
|
|
135
|
+
description="Create a website stream in graph8 first, then come back for its snippet."
|
|
136
|
+
className={className}
|
|
137
|
+
/>
|
|
138
|
+
);
|
|
139
|
+
|
|
140
|
+
return (
|
|
141
|
+
<div className={`space-y-4 ${className ?? ""}`}>
|
|
142
|
+
{list.length > 1 ? (
|
|
143
|
+
<label className="flex items-center gap-2 text-sm">
|
|
144
|
+
Website
|
|
145
|
+
<select
|
|
146
|
+
className="rounded border border-gray-300 px-2 py-1"
|
|
147
|
+
value={selected ?? s.stream_id ?? ""}
|
|
148
|
+
onChange={(e) => setSelected(e.target.value || undefined)}
|
|
149
|
+
>
|
|
150
|
+
{list.map((st) => (
|
|
151
|
+
<option key={st.id} value={st.id}>
|
|
152
|
+
{st.name}
|
|
153
|
+
{st.domains?.length ? ` (${st.domains[0]})` : ""}
|
|
154
|
+
</option>
|
|
155
|
+
))}
|
|
156
|
+
</select>
|
|
157
|
+
</label>
|
|
158
|
+
) : null}
|
|
159
|
+
{s.warning ? (
|
|
160
|
+
<p role="alert" className="rounded border border-amber-200 bg-amber-50 p-2 text-sm text-amber-900">
|
|
161
|
+
{s.warning}
|
|
162
|
+
</p>
|
|
163
|
+
) : null}
|
|
164
|
+
<CodeBlock label="React / Next.js" code={s.react_snippet} />
|
|
165
|
+
<CodeBlock label="Any website (script tag)" code={s.script_tag} />
|
|
166
|
+
<InstallCheck key={s.write_key} writeKey={s.write_key} onInstalled={onInstalled} />
|
|
167
|
+
</div>
|
|
168
|
+
);
|
|
169
|
+
}
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* TrafficChart: visitors, sessions and page views over time, with who is on the site right now and the
|
|
5
|
+
* pages they are on. Pure inline SVG, so it needs no chart library.
|
|
6
|
+
*
|
|
7
|
+
* SDK operations, in order:
|
|
8
|
+
* 1. getAudienceTrend GET /analytics/audience/trend (read; one row per day: users, sessions, page_views)
|
|
9
|
+
* 2. getRealtime GET /analytics/realtime (read; active users now, polled every 10 s)
|
|
10
|
+
* 3. getTopPages GET /analytics/behavior/pages (read; top pages by views)
|
|
11
|
+
*
|
|
12
|
+
* Tiers: all read; no confirmation. Realtime has no stream: the component re-reads every `realtimeSeconds`
|
|
13
|
+
* (default 10, the endpoint's cache TTL; 0 turns it off). Pass `writeKey` to scope to one website stream.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { useEffect, useState } from "react";
|
|
17
|
+
import { Button, Skeleton } from "@graph8/sdk/ui";
|
|
18
|
+
import { g8Get } from "@/g8/lib/g8-client";
|
|
19
|
+
import { useG8Query } from "@/g8/hooks/use-g8-query";
|
|
20
|
+
import { ApiError } from "@/g8/components/api-error";
|
|
21
|
+
import { EmptyState } from "@/g8/components/empty-state";
|
|
22
|
+
import { seriesOf } from "../lib/visitors-utils";
|
|
23
|
+
|
|
24
|
+
export interface TrafficChartProps {
|
|
25
|
+
days?: number;
|
|
26
|
+
writeKey?: string;
|
|
27
|
+
realtimeSeconds?: number;
|
|
28
|
+
className?: string;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
interface TrendPoint {
|
|
32
|
+
date: string;
|
|
33
|
+
users?: number;
|
|
34
|
+
new_users?: number;
|
|
35
|
+
sessions?: number;
|
|
36
|
+
page_views?: number;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
interface PageRow {
|
|
40
|
+
page_path?: string;
|
|
41
|
+
path?: string;
|
|
42
|
+
url?: string;
|
|
43
|
+
page_title?: string;
|
|
44
|
+
views?: number;
|
|
45
|
+
page_views?: number;
|
|
46
|
+
unique_visitors?: number;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
type Metric = "users" | "sessions" | "page_views";
|
|
50
|
+
const METRICS: Array<{ key: Metric; label: string }> = [
|
|
51
|
+
{ key: "users", label: "Visitors" },
|
|
52
|
+
{ key: "sessions", label: "Sessions" },
|
|
53
|
+
{ key: "page_views", label: "Page views" },
|
|
54
|
+
];
|
|
55
|
+
|
|
56
|
+
export function LineChart({ points, metric }: { points: TrendPoint[]; metric: Metric }) {
|
|
57
|
+
const width = 600;
|
|
58
|
+
const height = 160;
|
|
59
|
+
const values = points.map((p) => p[metric] ?? 0);
|
|
60
|
+
const max = Math.max(1, ...values);
|
|
61
|
+
const x = (i: number) => (points.length <= 1 ? width / 2 : (i / (points.length - 1)) * width);
|
|
62
|
+
const y = (v: number) => height - (v / max) * (height - 12) - 6;
|
|
63
|
+
const path = values.map((v, i) => `${i === 0 ? "M" : "L"}${x(i).toFixed(1)},${y(v).toFixed(1)}`).join(" ");
|
|
64
|
+
return (
|
|
65
|
+
<svg viewBox={`0 0 ${width} ${height}`} role="img" aria-label={`${metric} per day`} className="h-40 w-full text-blue-600">
|
|
66
|
+
<path d={path} fill="none" stroke="currentColor" strokeWidth="2" />
|
|
67
|
+
{values.map((v, i) => (
|
|
68
|
+
<circle key={points[i].date} cx={x(i)} cy={y(v)} r="2.5" fill="currentColor">
|
|
69
|
+
<title>{`${points[i].date}: ${v}`}</title>
|
|
70
|
+
</circle>
|
|
71
|
+
))}
|
|
72
|
+
</svg>
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function activeNow(envelope: unknown): number | undefined {
|
|
77
|
+
const data = (envelope as { data?: Record<string, unknown> } | null)?.data;
|
|
78
|
+
const value = data?.active_users_5min ?? data?.active_5min ?? data?.active_users ?? data?.five_min ?? data?.active_now;
|
|
79
|
+
return typeof value === "number" ? value : undefined;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export function TrafficChart({ days = 30, writeKey, realtimeSeconds = 10, className }: TrafficChartProps) {
|
|
83
|
+
const [metric, setMetric] = useState<Metric>("users");
|
|
84
|
+
const scope = { days, write_key: writeKey };
|
|
85
|
+
const trend = useG8Query((signal) => g8Get("/analytics/audience/trend", scope, { signal }), [days, writeKey]);
|
|
86
|
+
const pages = useG8Query((signal) => g8Get("/analytics/behavior/pages", { ...scope, limit: 8 }, { signal }), [days, writeKey]);
|
|
87
|
+
const live = useG8Query((signal) => g8Get("/analytics/realtime", { write_key: writeKey }, { signal }), [writeKey]);
|
|
88
|
+
|
|
89
|
+
useEffect(() => {
|
|
90
|
+
if (!realtimeSeconds) return;
|
|
91
|
+
const timer = setInterval(() => void live.refetch(), realtimeSeconds * 1000);
|
|
92
|
+
return () => clearInterval(timer);
|
|
93
|
+
}, [realtimeSeconds, live.refetch]);
|
|
94
|
+
|
|
95
|
+
const points = seriesOf<TrendPoint>(trend.data);
|
|
96
|
+
const topPages = seriesOf<PageRow>(pages.data);
|
|
97
|
+
const total = points.reduce((sum, p) => sum + (p[metric] ?? 0), 0);
|
|
98
|
+
const now = activeNow(live.data);
|
|
99
|
+
|
|
100
|
+
return (
|
|
101
|
+
<div className={`space-y-4 ${className ?? ""}`}>
|
|
102
|
+
<div className="flex flex-wrap items-center justify-between gap-2">
|
|
103
|
+
<div className="inline-flex gap-1" role="group" aria-label="Metric">
|
|
104
|
+
{METRICS.map((m) => (
|
|
105
|
+
<Button
|
|
106
|
+
key={m.key}
|
|
107
|
+
size="sm"
|
|
108
|
+
variant={metric === m.key ? "default" : "outline"}
|
|
109
|
+
aria-pressed={metric === m.key}
|
|
110
|
+
onClick={() => setMetric(m.key)}
|
|
111
|
+
>
|
|
112
|
+
{m.label}
|
|
113
|
+
</Button>
|
|
114
|
+
))}
|
|
115
|
+
</div>
|
|
116
|
+
<p className="text-sm text-gray-600" aria-live="polite">
|
|
117
|
+
{now != null ? `${now} on the site now` : ""}
|
|
118
|
+
</p>
|
|
119
|
+
</div>
|
|
120
|
+
|
|
121
|
+
{trend.error ? (
|
|
122
|
+
<ApiError error={trend.error} onRetry={() => void trend.refetch()} />
|
|
123
|
+
) : trend.loading && !trend.data ? (
|
|
124
|
+
<Skeleton className="h-40 w-full" />
|
|
125
|
+
) : points.length === 0 ? (
|
|
126
|
+
<EmptyState title="No traffic yet" description="Install the snippet and visits will chart here." />
|
|
127
|
+
) : (
|
|
128
|
+
<div>
|
|
129
|
+
<p className="text-sm text-gray-600">
|
|
130
|
+
{total.toLocaleString()} {METRICS.find((m) => m.key === metric)?.label.toLowerCase()} in {days} days
|
|
131
|
+
</p>
|
|
132
|
+
<LineChart points={points} metric={metric} />
|
|
133
|
+
</div>
|
|
134
|
+
)}
|
|
135
|
+
|
|
136
|
+
{topPages.length > 0 ? (
|
|
137
|
+
<table className="w-full text-sm">
|
|
138
|
+
<thead>
|
|
139
|
+
<tr className="text-left text-gray-500">
|
|
140
|
+
<th className="py-1">Top pages</th>
|
|
141
|
+
<th className="py-1 text-right">Views</th>
|
|
142
|
+
</tr>
|
|
143
|
+
</thead>
|
|
144
|
+
<tbody>
|
|
145
|
+
{topPages.map((p, i) => (
|
|
146
|
+
<tr key={i} className="border-t">
|
|
147
|
+
<td className="py-1">{p.page_path ?? p.path ?? p.url ?? p.page_title ?? "-"}</td>
|
|
148
|
+
<td className="py-1 text-right">{(p.views ?? p.page_views ?? 0).toLocaleString()}</td>
|
|
149
|
+
</tr>
|
|
150
|
+
))}
|
|
151
|
+
</tbody>
|
|
152
|
+
</table>
|
|
153
|
+
) : null}
|
|
154
|
+
</div>
|
|
155
|
+
);
|
|
156
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* VisitorCompanyCard: one company that visited your site, with the pages it viewed and, on request,
|
|
5
|
+
* its full journey.
|
|
6
|
+
*
|
|
7
|
+
* SDK operations, in order:
|
|
8
|
+
* 1. listCompanyVisitorEvents GET /visitors/companies/events (read; only when "Full journey" is clicked,
|
|
9
|
+
* with filter={"company_id": <id>} and show_mode=raw)
|
|
10
|
+
*
|
|
11
|
+
* Tiers: read only; no confirmation. Pass a grouped visit from `VisitorFeed` (or any
|
|
12
|
+
* `/visitors/companies/events?show_mode=grouped` row) as `visit`. `actions` slot: put your own buttons
|
|
13
|
+
* ("Add to list", "Open company") next to the name.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { useState } from "react";
|
|
17
|
+
import type { ReactNode } from "react";
|
|
18
|
+
import { Button, Card, CardContent, CardHeader, CardTitle, Skeleton } from "@graph8/sdk/ui";
|
|
19
|
+
import { g8Get } from "@/g8/lib/g8-client";
|
|
20
|
+
import { useG8Query } from "@/g8/hooks/use-g8-query";
|
|
21
|
+
import { ApiError } from "@/g8/components/api-error";
|
|
22
|
+
import { companyLabel, pageLabel, pageOf, timeAgo } from "../lib/visitors-utils";
|
|
23
|
+
import type { FlowEvent, VisitorGroup } from "../lib/visitors-utils";
|
|
24
|
+
|
|
25
|
+
export interface VisitorCompanyCardProps {
|
|
26
|
+
visit: VisitorGroup;
|
|
27
|
+
/** Pages shown before "Full journey". Default 5. */
|
|
28
|
+
previewPages?: number;
|
|
29
|
+
actions?: ReactNode;
|
|
30
|
+
className?: string;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function PageList({ pages }: { pages: FlowEvent[] }) {
|
|
34
|
+
if (pages.length === 0) return <p className="text-sm text-gray-500">No page views recorded.</p>;
|
|
35
|
+
return (
|
|
36
|
+
<ol className="space-y-1 text-sm">
|
|
37
|
+
{pages.map((p, i) => (
|
|
38
|
+
<li key={p.message_id ?? `${p.received_at}-${i}`} className="flex justify-between gap-3">
|
|
39
|
+
<span className="truncate">
|
|
40
|
+
{pageLabel(p)}
|
|
41
|
+
{p.page_referrer ? <span className="ml-2 text-xs text-gray-400">from {p.page_referrer}</span> : null}
|
|
42
|
+
</span>
|
|
43
|
+
<span className="shrink-0 text-xs text-gray-500">{timeAgo(p.received_at)}</span>
|
|
44
|
+
</li>
|
|
45
|
+
))}
|
|
46
|
+
</ol>
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function Journey({ companyId }: { companyId: number }) {
|
|
51
|
+
const journey = useG8Query(
|
|
52
|
+
(signal) =>
|
|
53
|
+
g8Get("/visitors/companies/events", { show_mode: "raw", filter: JSON.stringify({ company_id: companyId }), limit: 100 }, { signal }),
|
|
54
|
+
[companyId],
|
|
55
|
+
);
|
|
56
|
+
if (journey.loading) return <Skeleton className="h-16 w-full" />;
|
|
57
|
+
if (journey.error) return <ApiError error={journey.error} onRetry={() => void journey.refetch()} />;
|
|
58
|
+
// Raw mode returns one row per page view.
|
|
59
|
+
const views = pageOf(journey.data).events as unknown as FlowEvent[];
|
|
60
|
+
return <PageList pages={views} />;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function VisitorCompanyCard({ visit, previewPages = 5, actions, className }: VisitorCompanyCardProps) {
|
|
64
|
+
const [showJourney, setShowJourney] = useState(false);
|
|
65
|
+
const pages = visit.children ?? [];
|
|
66
|
+
return (
|
|
67
|
+
<Card className={className} data-testid="visitor-company-card">
|
|
68
|
+
<CardHeader className="flex flex-row items-start justify-between gap-3">
|
|
69
|
+
<div>
|
|
70
|
+
<CardTitle className="text-base">{companyLabel(visit)}</CardTitle>
|
|
71
|
+
<p className="text-xs text-gray-500">
|
|
72
|
+
{visit.COMPANY_DOMAIN ? `${visit.COMPANY_DOMAIN} - ` : ""}last seen {timeAgo(visit.received_at)} - {pages.length} page view
|
|
73
|
+
{pages.length === 1 ? "" : "s"}
|
|
74
|
+
</p>
|
|
75
|
+
</div>
|
|
76
|
+
{actions ? <div className="flex gap-2">{actions}</div> : null}
|
|
77
|
+
</CardHeader>
|
|
78
|
+
<CardContent className="space-y-3">
|
|
79
|
+
{showJourney && visit.COMPANY_ID != null ? (
|
|
80
|
+
<Journey companyId={visit.COMPANY_ID} />
|
|
81
|
+
) : (
|
|
82
|
+
<PageList pages={pages.slice(0, previewPages)} />
|
|
83
|
+
)}
|
|
84
|
+
{visit.COMPANY_ID != null ? (
|
|
85
|
+
<Button variant="link" size="sm" className="px-0" onClick={() => setShowJourney((v) => !v)}>
|
|
86
|
+
{showJourney ? "Show summary" : "Full journey"}
|
|
87
|
+
</Button>
|
|
88
|
+
) : null}
|
|
89
|
+
</CardContent>
|
|
90
|
+
</Card>
|
|
91
|
+
);
|
|
92
|
+
}
|