@graph8/sdk 0.370.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.
- package/README.md +22 -3
- package/blocks/_foundation/README.md +61 -0
- package/blocks/_foundation/block.json +57 -0
- package/blocks/_foundation/components/auth-gate.tsx +363 -0
- package/blocks/_foundation/components/confirm-spend-dialog.tsx +15 -8
- package/blocks/_foundation/components/empty-state.tsx +5 -2
- package/blocks/_foundation/components/list-select.tsx +112 -0
- package/blocks/_foundation/components/paging-notices.tsx +55 -0
- package/blocks/_foundation/components/screen/detail-panel.tsx +2 -1
- package/blocks/_foundation/components/screen/filter-controls.tsx +92 -0
- package/blocks/_foundation/components/screen/index.ts +1 -0
- package/blocks/_foundation/components/screen/split-view.tsx +1 -1
- package/blocks/_foundation/hooks/use-all-pages.ts +54 -0
- package/blocks/_foundation/hooks/use-load-more-list.ts +61 -0
- package/blocks/_foundation/hooks/use-url-filters.ts +93 -0
- package/blocks/_foundation/lib/paging.ts +57 -0
- package/blocks/_foundation/lib/use-debounced-value.ts +13 -0
- package/blocks/_foundation/server/auth.ts +815 -0
- package/blocks/agents/README.md +81 -4
- package/blocks/agents/block.json +311 -2
- package/blocks/agents/components/agent-activity.tsx +112 -0
- package/blocks/agents/components/agent-approvals.tsx +135 -0
- package/blocks/agents/components/agent-builder.tsx +17 -3
- package/blocks/agents/components/agent-conversations.tsx +143 -0
- package/blocks/agents/components/agent-knowledge-panel.tsx +2 -2
- package/blocks/agents/components/agent-operators.tsx +164 -0
- package/blocks/agents/components/agent-run-detail.tsx +20 -3
- package/blocks/agents/components/agent-runs.tsx +3 -3
- package/blocks/agents/components/agent-skills-panel.tsx +7 -7
- package/blocks/agents/components/agent-test-history.tsx +121 -0
- package/blocks/agents/components/agents-screen.tsx +53 -11
- package/blocks/agents/components/assistant-sessions.tsx +150 -0
- package/blocks/agents/components/duplicate-agent-button.tsx +42 -0
- package/blocks/agents/components/from-template-button.tsx +65 -0
- package/blocks/agents/components/knowledge-browser.tsx +107 -0
- package/blocks/agents/components/knowledge-detail.tsx +156 -0
- package/blocks/agents/components/knowledge-form.tsx +69 -0
- package/blocks/agents/components/name-dialog.tsx +32 -0
- package/blocks/agents/components/receptionist-button.tsx +46 -0
- package/blocks/agents/components/save-as-template-button.tsx +32 -0
- package/blocks/agents/components/skill-editor.tsx +147 -0
- package/blocks/agents/components/skill-library.tsx +111 -0
- package/blocks/agents/lib/agent-types.ts +7 -1
- package/blocks/agents/lib/facts.tsx +13 -0
- package/blocks/agents/lib/knowledge-utils.ts +49 -0
- package/blocks/agents/lib/load-more.tsx +141 -0
- package/blocks/agents/lib/skill-utils.ts +91 -0
- package/blocks/agents/recipes/create-agent-and-attach-skill.ts +1 -1
- package/blocks/analytics/components/analytics-screen.tsx +16 -3
- package/blocks/analytics/components/kpi-tiles.tsx +1 -1
- package/blocks/analytics/components/report-builder.tsx +15 -2
- package/blocks/appointments/README.md +6 -0
- package/blocks/appointments/block.json +27 -0
- package/blocks/appointments/components/booking-page.tsx +2 -2
- package/blocks/appointments/components/meeting-contact-links.tsx +49 -0
- package/blocks/appointments/components/meeting-detail.tsx +2 -2
- package/blocks/appointments/components/meeting-list.tsx +29 -13
- package/blocks/appointments/components/record-meetings-tab.tsx +92 -0
- package/blocks/campaigns/README.md +1 -1
- package/blocks/campaigns/block.json +34 -1
- package/blocks/campaigns/components/campaign-assets.tsx +1 -1
- package/blocks/campaigns/components/campaign-launch-step.tsx +16 -11
- package/blocks/campaigns/components/campaign-links.tsx +180 -0
- package/blocks/campaigns/components/campaign-metrics.tsx +1 -1
- package/blocks/campaigns/components/campaign-wizard.tsx +10 -5
- package/blocks/campaigns/components/icp-detail.tsx +11 -2
- package/blocks/campaigns/components/icp-editor.tsx +8 -3
- package/blocks/campaigns/components/persona-detail.tsx +11 -2
- package/blocks/campaigns/components/persona-editor.tsx +6 -1
- package/blocks/campaigns/lib/campaigns-utils.ts +4 -4
- package/blocks/coach/components/call-scorecard.tsx +7 -7
- package/blocks/company-intelligence/README.md +132 -0
- package/blocks/company-intelligence/block.json +318 -0
- package/blocks/company-intelligence/components/company-intelligence-screen.tsx +114 -0
- package/blocks/company-intelligence/components/company-intelligence-tab.tsx +88 -0
- package/blocks/company-intelligence/components/company-profile.tsx +68 -0
- package/blocks/company-intelligence/components/intelligence-sources.tsx +63 -0
- package/blocks/company-intelligence/components/report-detail.tsx +189 -0
- package/blocks/company-intelligence/components/research-reports.tsx +136 -0
- package/blocks/company-intelligence/components/research-run.tsx +165 -0
- package/blocks/company-intelligence/components/source-detail.tsx +117 -0
- package/blocks/company-intelligence/components/source-view.tsx +274 -0
- package/blocks/company-intelligence/components/wiki-document-detail.tsx +116 -0
- package/blocks/company-intelligence/components/wiki-documents.tsx +66 -0
- package/blocks/company-intelligence/hooks/use-intelligence.ts +173 -0
- package/blocks/company-intelligence/lib/intelligence-types.ts +230 -0
- package/blocks/company-intelligence/lib/safe-markdown.tsx +150 -0
- package/blocks/company-intelligence/recipes/read-competitors-and-news.ts +33 -0
- package/blocks/company-intelligence/recipes/research-your-website.ts +43 -0
- package/blocks/company-intelligence/recipes/write-the-research-reports.ts +54 -0
- package/blocks/copilot/components/copilot-memory.tsx +6 -2
- package/blocks/copilot/components/copilot-panel.tsx +1 -1
- package/blocks/copilot/components/copilot-team-chat.tsx +6 -1
- package/blocks/crm/README.md +25 -0
- package/blocks/crm/block.json +257 -9
- package/blocks/crm/components/bulk-update.tsx +119 -0
- package/blocks/crm/components/companies-screen.tsx +23 -9
- package/blocks/crm/components/company-detail.tsx +2 -2
- package/blocks/crm/components/contact-detail.tsx +4 -4
- package/blocks/crm/components/contact-documents-tab.tsx +144 -0
- package/blocks/crm/components/contact-insights-tab.tsx +118 -0
- package/blocks/crm/components/contact-lookup-link.tsx +85 -0
- package/blocks/crm/components/contact-row-actions.tsx +43 -0
- package/blocks/crm/components/contact-tag-chips.tsx +113 -0
- package/blocks/crm/components/contacts-screen.tsx +77 -18
- package/blocks/crm/components/dedupe-review.tsx +6 -1
- package/blocks/crm/components/import-wizard.tsx +3 -7
- package/blocks/crm/components/list-select.tsx +20 -0
- package/blocks/crm/components/owner-assign.tsx +132 -0
- package/blocks/crm/components/record-engagement-tab.tsx +103 -0
- package/blocks/crm/components/record-form.tsx +1 -1
- package/blocks/crm/components/record-open-links.tsx +79 -0
- package/blocks/crm/components/tag-assign.tsx +112 -0
- package/blocks/crm/components/tag-manager.tsx +178 -0
- package/blocks/crm/components/tasks-screen.tsx +35 -15
- package/blocks/crm/components/unlock-info-button.tsx +82 -0
- package/blocks/crm/components/verify-email-button.tsx +93 -0
- package/blocks/crm/hooks/use-all-lists.ts +18 -0
- package/blocks/crm/hooks/use-contact-tags.ts +29 -0
- package/blocks/crm/hooks/use-page-query.ts +3 -1
- package/blocks/crm/lib/crm-types.ts +14 -0
- package/blocks/crm/lib/read-lists.ts +16 -0
- package/blocks/crm/lib/record-actions.ts +102 -0
- package/blocks/deals/README.md +25 -6
- package/blocks/deals/block.json +168 -2
- package/blocks/deals/components/close-deal-dialog.tsx +169 -0
- package/blocks/deals/components/create-deal-button.tsx +109 -0
- package/blocks/deals/components/deal-contacts-editor.tsx +134 -0
- package/blocks/deals/components/deal-detail.tsx +7 -5
- package/blocks/deals/components/deal-filters.tsx +65 -0
- package/blocks/deals/components/deal-line-items.tsx +120 -0
- package/blocks/deals/components/deal-notes-tab.tsx +97 -0
- package/blocks/deals/components/deal-related.tsx +30 -9
- package/blocks/deals/components/deal-tasks-tab.tsx +88 -0
- package/blocks/deals/components/deals-screen.tsx +57 -22
- package/blocks/deals/components/forecast-view.tsx +4 -4
- package/blocks/deals/components/pipeline-board.tsx +8 -6
- package/blocks/deals/components/pipeline-editor.tsx +132 -0
- package/blocks/deals/components/record-deals-tab.tsx +62 -0
- package/blocks/deals/components/stage-editor.tsx +1 -1
- package/blocks/deals/hooks/use-pipeline-deals.ts +17 -4
- package/blocks/deals/lib/deal-extras.ts +156 -0
- package/blocks/dialer/README.md +14 -5
- package/blocks/dialer/block.json +17 -2
- package/blocks/dialer/components/call-contact-button.tsx +26 -9
- package/blocks/dialer/components/call-log-screen.tsx +10 -2
- package/blocks/dialer/components/call-log.tsx +16 -4
- package/blocks/dialer/components/call-queue.tsx +3 -3
- package/blocks/dialer/components/dialer-screen.tsx +69 -27
- package/blocks/dialer/components/disposition-panel.tsx +1 -1
- package/blocks/dialer/components/record-calls-tab.tsx +101 -0
- package/blocks/dialer/components/session-start-form.tsx +58 -11
- package/blocks/dialer/components/softphone.tsx +1 -1
- package/blocks/documents/README.md +86 -0
- package/blocks/documents/block.json +378 -0
- package/blocks/documents/components/account-bar.tsx +48 -0
- package/blocks/documents/components/correspondence-form.tsx +95 -0
- package/blocks/documents/components/correspondence-import.tsx +106 -0
- package/blocks/documents/components/correspondence-panel.tsx +191 -0
- package/blocks/documents/components/documents-screen.tsx +76 -0
- package/blocks/documents/components/library-document-detail.tsx +175 -0
- package/blocks/documents/components/library-document-form.tsx +105 -0
- package/blocks/documents/components/library-folder-form.tsx +74 -0
- package/blocks/documents/components/library-panel.tsx +235 -0
- package/blocks/documents/components/library-upload.tsx +160 -0
- package/blocks/documents/components/question-row.tsx +130 -0
- package/blocks/documents/components/questionnaire-detail.tsx +197 -0
- package/blocks/documents/components/questionnaires-panel.tsx +173 -0
- package/blocks/documents/components/record-documents-tab.tsx +109 -0
- package/blocks/documents/lib/documents-types.ts +252 -0
- package/blocks/documents/lib/download.ts +64 -0
- package/blocks/documents/recipes/follow-a-questionnaire.ts +26 -0
- package/blocks/documents/recipes/log-account-correspondence.ts +27 -0
- package/blocks/documents/recipes/organize-the-library.ts +25 -0
- package/blocks/email-signatures/README.md +123 -0
- package/blocks/email-signatures/block.json +341 -0
- package/blocks/email-signatures/components/email-signatures-screen.tsx +166 -0
- package/blocks/email-signatures/components/signature-assign-template.tsx +119 -0
- package/blocks/email-signatures/components/signature-block-editor.tsx +87 -0
- package/blocks/email-signatures/components/signature-campaign-form.tsx +165 -0
- package/blocks/email-signatures/components/signature-campaigns.tsx +75 -0
- package/blocks/email-signatures/components/signature-identity-form.tsx +131 -0
- package/blocks/email-signatures/components/signature-mailboxes.tsx +195 -0
- package/blocks/email-signatures/components/signature-policy.tsx +130 -0
- package/blocks/email-signatures/components/signature-preview.tsx +119 -0
- package/blocks/email-signatures/components/signature-report.tsx +77 -0
- package/blocks/email-signatures/components/signature-template-details.tsx +112 -0
- package/blocks/email-signatures/components/signature-template-form.tsx +109 -0
- package/blocks/email-signatures/hooks/use-signatures-data.ts +93 -0
- package/blocks/email-signatures/lib/signatures-types.ts +392 -0
- package/blocks/email-signatures/recipes/assign-a-signature-to-a-new-hire.ts +32 -0
- package/blocks/email-signatures/recipes/create-a-template-and-make-it-default.ts +35 -0
- package/blocks/email-signatures/recipes/signature-engagement-report.ts +16 -0
- package/blocks/enrichment/README.md +38 -10
- package/blocks/enrichment/block.json +164 -12
- package/blocks/enrichment/components/auto-enrichment-configs.tsx +134 -0
- package/blocks/enrichment/components/auto-enrichment-settings.tsx +82 -0
- package/blocks/enrichment/components/bulk-enrich-panel.tsx +79 -30
- package/blocks/enrichment/components/email-verify-badge.tsx +3 -3
- package/blocks/enrichment/components/enrich-button.tsx +1 -1
- package/blocks/enrichment/components/enrichment-activity.tsx +93 -0
- package/blocks/enrichment/components/enrichment-job-detail.tsx +18 -0
- package/blocks/enrichment/components/enrichment-screen.tsx +103 -19
- package/blocks/enrichment/components/list-pipelines.tsx +97 -0
- package/blocks/enrichment/components/pipeline-detail.tsx +214 -0
- package/blocks/enrichment/components/pipeline-editor.tsx +144 -0
- package/blocks/enrichment/components/pipeline-templates.tsx +90 -0
- package/blocks/enrichment/components/waterfall-config.tsx +27 -7
- package/blocks/enrichment/lib/enrichment-types.ts +51 -0
- package/blocks/enrichment/lib/pipeline-types.ts +234 -0
- package/blocks/enrichment/recipes/bulk-enrich-and-poll.ts +15 -4
- package/blocks/forms/README.md +10 -0
- package/blocks/forms/block.json +107 -0
- package/blocks/forms/components/form-activity.tsx +71 -0
- package/blocks/forms/components/form-backfill.tsx +108 -0
- package/blocks/forms/components/form-builder.tsx +1 -1
- package/blocks/forms/components/form-embed.tsx +1 -1
- package/blocks/forms/components/form-groups.tsx +151 -0
- package/blocks/forms/components/form-mapping-editor.tsx +6 -9
- package/blocks/forms/components/form-route-history.tsx +72 -0
- package/blocks/forms/components/form-saved-views.tsx +81 -0
- package/blocks/forms/components/forms-screen.tsx +57 -20
- package/blocks/forms/components/more-rows.tsx +30 -0
- package/blocks/forms/components/submissions-export.tsx +67 -0
- package/blocks/forms/components/submissions-table.tsx +15 -6
- package/blocks/forms/lib/forms-extra.ts +67 -0
- package/blocks/inbox/README.md +45 -4
- package/blocks/inbox/block.json +145 -8
- package/blocks/inbox/components/blocked-senders.tsx +103 -0
- package/blocks/inbox/components/compose-message.tsx +193 -0
- package/blocks/inbox/components/inbox-screen.tsx +54 -6
- package/blocks/inbox/components/inbox-search.tsx +106 -0
- package/blocks/inbox/components/record-conversations-tab.tsx +154 -0
- package/blocks/inbox/components/tag-manager.tsx +136 -0
- package/blocks/inbox/components/thread-actions.tsx +133 -0
- package/blocks/inbox/components/thread-badge.tsx +2 -2
- package/blocks/inbox/components/thread-view.tsx +3 -1
- package/blocks/inbox/hooks/use-inbox-mailboxes.ts +31 -0
- package/blocks/inbox/lib/inbox-manage.ts +227 -0
- package/blocks/integrations/README.md +54 -1
- package/blocks/integrations/block.json +189 -15
- package/blocks/integrations/components/ad-accounts.tsx +82 -0
- package/blocks/integrations/components/connection-health.tsx +59 -0
- package/blocks/integrations/components/crm-detail.tsx +20 -1
- package/blocks/integrations/components/crm-records.tsx +103 -0
- package/blocks/integrations/components/field-mappings.tsx +61 -0
- package/blocks/integrations/components/integration-list.tsx +4 -4
- package/blocks/integrations/components/integrations-screen.tsx +20 -3
- package/blocks/integrations/components/migration-panel.tsx +133 -0
- package/blocks/integrations/components/sugarcrm-connect.tsx +95 -0
- package/blocks/integrations/components/sync-status.tsx +6 -6
- package/blocks/integrations/components/sync-triggers.tsx +134 -0
- package/blocks/integrations/components/webhook-detail.tsx +26 -7
- package/blocks/integrations/components/webhook-manager.tsx +30 -17
- package/blocks/integrations/components/workspace-tools.tsx +127 -0
- package/blocks/integrations/lib/crm-utils.ts +39 -0
- package/blocks/integrations/lib/facts.tsx +13 -0
- package/blocks/integrations/lib/integrations-utils.ts +5 -0
- package/blocks/integrations/lib/load-more.tsx +141 -0
- package/blocks/intent/components/account-intent-score.tsx +5 -5
- package/blocks/intent/components/intent-feed.tsx +8 -8
- package/blocks/intent/components/intent-keyword-manager.tsx +8 -8
- package/blocks/intent/lib/intent-utils.ts +4 -4
- package/blocks/lifecycle/components/account-related.tsx +8 -1
- package/blocks/lifecycle/components/churn-list.tsx +5 -2
- package/blocks/lifecycle/components/customer-table.tsx +1 -1
- package/blocks/lists/README.md +7 -1
- package/blocks/lists/block.json +31 -3
- package/blocks/lists/components/add-company-people-to-list-button.tsx +73 -0
- package/blocks/lists/components/add-to-list-button.tsx +17 -7
- package/blocks/lists/components/list-picker.tsx +13 -3
- package/blocks/lists/components/list-table.tsx +1 -1
- package/blocks/lists/components/lists-screen.tsx +4 -2
- package/blocks/lists/components/record-lists-tab.tsx +99 -0
- package/blocks/lists/components/suppression-manager.tsx +27 -6
- package/blocks/lists/components/suppressions-screen.tsx +21 -12
- package/blocks/mailboxes/block.json +4 -0
- package/blocks/mailboxes/components/connect-mailbox.tsx +9 -8
- package/blocks/mailboxes/components/domain-detail.tsx +10 -4
- package/blocks/mailboxes/components/domain-purchase.tsx +14 -11
- package/blocks/mailboxes/components/mailbox-detail.tsx +3 -2
- package/blocks/mailboxes/components/mailbox-list.tsx +8 -3
- package/blocks/mailboxes/components/mailboxes-screen.tsx +8 -3
- package/blocks/mailboxes/components/warmup-health.tsx +20 -20
- package/blocks/mailboxes/lib/mailboxes-utils.ts +4 -4
- package/blocks/mailboxes/lib/use-provider-domains.ts +23 -0
- package/blocks/newsletter/README.md +12 -0
- package/blocks/newsletter/block.json +117 -0
- package/blocks/newsletter/components/issue-scheduler.tsx +2 -2
- package/blocks/newsletter/components/issue-versions.tsx +93 -0
- package/blocks/newsletter/components/more-rows.tsx +30 -0
- package/blocks/newsletter/components/newsletter-editor.tsx +1 -1
- package/blocks/newsletter/components/newsletter-list-syncs.tsx +121 -0
- package/blocks/newsletter/components/newsletter-screen.tsx +38 -13
- package/blocks/newsletter/components/newsletter-settings.tsx +211 -0
- package/blocks/newsletter/components/newsletter-stats.tsx +2 -2
- package/blocks/newsletter/components/newsletter-templates.tsx +133 -0
- package/blocks/newsletter/components/subscriber-table.tsx +5 -7
- package/blocks/newsletter/components/subscriber-timeline.tsx +65 -0
- package/blocks/newsletter/lib/newsletter-extra.ts +60 -0
- package/blocks/nurture/components/nurture-builder.tsx +10 -2
- package/blocks/nurture/components/nurture-enroll.tsx +6 -7
- package/blocks/nurture/components/nurture-form.tsx +5 -8
- package/blocks/nurture/components/nurture-screen.tsx +16 -5
- package/blocks/nurture/components/nurture-stats.tsx +5 -4
- package/blocks/objects/README.md +1 -1
- package/blocks/objects/block.json +13 -5
- package/blocks/objects/components/attribute-field-input.tsx +14 -1
- package/blocks/objects/components/object-record-detail.tsx +8 -1
- package/blocks/objects/components/object-schema-editor.tsx +1 -1
- package/blocks/objects/components/record-roles-panel.tsx +15 -5
- package/blocks/pages/README.md +14 -0
- package/blocks/pages/block.json +123 -2
- package/blocks/pages/components/more-rows.tsx +30 -0
- package/blocks/pages/components/page-ai-tools.tsx +196 -0
- package/blocks/pages/components/page-analytics.tsx +176 -0
- package/blocks/pages/components/page-clone.tsx +99 -0
- package/blocks/pages/components/page-domains.tsx +167 -0
- package/blocks/pages/components/page-editor.tsx +1 -1
- package/blocks/pages/components/page-generation-status.tsx +48 -0
- package/blocks/pages/components/page-tracking.tsx +137 -0
- package/blocks/pages/components/pages-screen.tsx +57 -16
- package/blocks/pages/components/seo-report.tsx +6 -1
- package/blocks/pages/hooks/use-landing-page-paths.ts +60 -0
- package/blocks/pages/lib/studio.ts +7 -0
- package/blocks/phone-numbers/README.md +74 -0
- package/blocks/phone-numbers/block.json +262 -0
- package/blocks/phone-numbers/components/a2p-registration.tsx +230 -0
- package/blocks/phone-numbers/components/assign-number.tsx +104 -0
- package/blocks/phone-numbers/components/number-search.tsx +239 -0
- package/blocks/phone-numbers/components/number-settings.tsx +134 -0
- package/blocks/phone-numbers/components/number-usage.tsx +97 -0
- package/blocks/phone-numbers/components/phone-number-detail.tsx +84 -0
- package/blocks/phone-numbers/components/phone-number-list.tsx +110 -0
- package/blocks/phone-numbers/components/phone-numbers-screen.tsx +201 -0
- package/blocks/phone-numbers/components/register-owned-number.tsx +98 -0
- package/blocks/phone-numbers/components/release-number.tsx +126 -0
- package/blocks/phone-numbers/hooks/use-phone-numbers.ts +58 -0
- package/blocks/phone-numbers/lib/phone-numbers-utils.ts +202 -0
- package/blocks/phone-numbers/recipes/01-search-and-buy-number.ts +49 -0
- package/blocks/phone-numbers/recipes/02-assign-number-to-rep.ts +25 -0
- package/blocks/phone-numbers/recipes/03-audit-numbers.ts +36 -0
- package/blocks/phone-numbers/recipes/04-register-text-messaging.ts +39 -0
- package/blocks/pickers/README.md +23 -0
- package/blocks/pickers/block.json +103 -0
- package/blocks/pickers/components/company-picker.tsx +90 -0
- package/blocks/pickers/components/contact-picker.tsx +131 -0
- package/blocks/pickers/components/member-picker.tsx +99 -0
- package/blocks/pickers/components/search-picker.tsx +98 -0
- package/blocks/pickers/hooks/use-current-member.ts +104 -0
- package/blocks/pickers/hooks/use-picker-pages.ts +73 -0
- package/blocks/pickers/lib/use-debounced.ts +13 -0
- package/blocks/programmatic-pages/README.md +82 -0
- package/blocks/programmatic-pages/block.json +292 -0
- package/blocks/programmatic-pages/components/programmatic-campaign-form.tsx +136 -0
- package/blocks/programmatic-pages/components/programmatic-generate.tsx +112 -0
- package/blocks/programmatic-pages/components/programmatic-generated-pages.tsx +210 -0
- package/blocks/programmatic-pages/components/programmatic-overview.tsx +84 -0
- package/blocks/programmatic-pages/components/programmatic-pages-screen.tsx +221 -0
- package/blocks/programmatic-pages/components/programmatic-preview.tsx +66 -0
- package/blocks/programmatic-pages/components/programmatic-publish.tsx +102 -0
- package/blocks/programmatic-pages/components/programmatic-settings.tsx +96 -0
- package/blocks/programmatic-pages/components/programmatic-source-fields.tsx +75 -0
- package/blocks/programmatic-pages/components/programmatic-source.tsx +106 -0
- package/blocks/programmatic-pages/components/programmatic-variables.tsx +84 -0
- package/blocks/programmatic-pages/hooks/use-programmatic-workspace.ts +19 -0
- package/blocks/programmatic-pages/lib/programmatic-types.ts +330 -0
- package/blocks/programmatic-pages/recipes/create-a-programmatic-campaign.ts +23 -0
- package/blocks/programmatic-pages/recipes/render-and-follow.ts +21 -0
- package/blocks/programmatic-pages/recipes/review-then-publish.ts +17 -0
- package/blocks/quotes/README.md +38 -1
- package/blocks/quotes/block.json +170 -6
- package/blocks/quotes/components/create-quote-from-deal.tsx +48 -0
- package/blocks/quotes/components/manual-products.tsx +144 -0
- package/blocks/quotes/components/quote-batch-actions.tsx +86 -0
- package/blocks/quotes/components/quote-builder.tsx +1 -1
- package/blocks/quotes/components/quote-documents.tsx +94 -0
- package/blocks/quotes/components/quote-evidence.tsx +95 -0
- package/blocks/quotes/components/quote-lifecycle-actions.tsx +146 -0
- package/blocks/quotes/components/quote-list.tsx +28 -8
- package/blocks/quotes/components/quote-related.tsx +3 -2
- package/blocks/quotes/components/quote-settings.tsx +184 -0
- package/blocks/quotes/components/quote-templates.tsx +189 -0
- package/blocks/quotes/components/quote-viewer.tsx +23 -5
- package/blocks/quotes/components/quotes-screen.tsx +15 -5
- package/blocks/quotes/components/record-quotes-tab.tsx +90 -0
- package/blocks/quotes/lib/quote-admin.ts +264 -0
- package/blocks/radar/components/competitor-battle-card.tsx +1 -1
- package/blocks/radar/components/radar-overview.tsx +4 -4
- package/blocks/radar/components/radar-screen.tsx +2 -2
- package/blocks/sequencer/README.md +61 -0
- package/blocks/sequencer/block.json +200 -6
- package/blocks/sequencer/components/add-to-sequence-button.tsx +82 -0
- package/blocks/sequencer/components/enroll-dialog.tsx +2 -2
- package/blocks/sequencer/components/enrollment-withdraw.tsx +71 -0
- package/blocks/sequencer/components/record-sequences-tab.tsx +126 -0
- package/blocks/sequencer/components/sequence-builder.tsx +32 -23
- package/blocks/sequencer/components/sequence-campaign-link.tsx +78 -0
- package/blocks/sequencer/components/sequence-detail.tsx +10 -2
- package/blocks/sequencer/components/sequence-edit-form.tsx +111 -0
- package/blocks/sequencer/components/sequence-enrollments.tsx +5 -2
- package/blocks/sequencer/components/sequence-list.tsx +1 -1
- package/blocks/sequencer/components/sequence-manage-actions.tsx +119 -0
- package/blocks/sequencer/components/sequence-senders-panel.tsx +84 -0
- package/blocks/sequencer/components/sequence-stats.tsx +8 -8
- package/blocks/sequencer/components/sequence-templates.tsx +160 -0
- package/blocks/sequencer/components/sequences-screen.tsx +39 -6
- package/blocks/sequencer/components/step-ai-assist.tsx +196 -0
- package/blocks/sequencer/components/step-editor.tsx +21 -12
- package/blocks/sequencer/components/step-templates.tsx +224 -0
- package/blocks/sequencer/lib/sequencer-manage.ts +235 -0
- package/blocks/sequencer/lib/sequencer-utils.ts +9 -5
- package/blocks/signals/README.md +39 -0
- package/blocks/signals/block.json +287 -0
- package/blocks/signals/components/auto-seed-panel.tsx +111 -0
- package/blocks/signals/components/listener-form.tsx +184 -0
- package/blocks/signals/components/matched-posts.tsx +195 -0
- package/blocks/signals/components/post-engagements.tsx +114 -0
- package/blocks/signals/components/record-social-tab.tsx +49 -0
- package/blocks/signals/components/social-listeners-screen.tsx +250 -0
- package/blocks/signals/components/social-settings.tsx +128 -0
- package/blocks/signals/components/source-lists.tsx +83 -0
- package/blocks/signals/components/top-engagers.tsx +68 -0
- package/blocks/signals/components/x-integration.tsx +129 -0
- package/blocks/signals/lib/signals-types.ts +228 -0
- package/blocks/signals/recipes/preview-and-create-a-listener.ts +16 -0
- package/blocks/signals/recipes/spend-against-caps.ts +14 -0
- package/blocks/signals/recipes/triage-matched-posts.ts +18 -0
- package/blocks/social-ads/README.md +8 -0
- package/blocks/social-ads/block.json +137 -4
- package/blocks/social-ads/components/audience-sync-detail.tsx +9 -3
- package/blocks/social-ads/components/audience-sync.tsx +6 -2
- package/blocks/social-ads/components/campaign-ads.tsx +166 -0
- package/blocks/social-ads/components/campaign-detail.tsx +6 -1
- package/blocks/social-ads/components/campaign-editor.tsx +134 -0
- package/blocks/social-ads/components/creative-concepts.tsx +74 -0
- package/blocks/social-ads/components/creative-detail.tsx +6 -1
- package/blocks/social-ads/components/creative-gallery.tsx +20 -2
- package/blocks/social-ads/components/creative-revisions.tsx +57 -0
- package/blocks/social-ads/components/creative-tools.tsx +176 -0
- package/blocks/social-ads/components/post-composer.tsx +4 -2
- package/blocks/social-ads/components/post-detail.tsx +3 -0
- package/blocks/social-ads/components/social-ideas.tsx +64 -0
- package/blocks/social-ads/components/social-performance.tsx +80 -0
- package/blocks/social-ads/components/social-screen.tsx +33 -6
- package/blocks/social-ads/lib/ads-extra.ts +67 -0
- package/blocks/surveys/README.md +91 -0
- package/blocks/surveys/block.json +299 -0
- package/blocks/surveys/components/account-nps.tsx +69 -0
- package/blocks/surveys/components/contact-survey-history.tsx +52 -0
- package/blocks/surveys/components/org-summary.tsx +82 -0
- package/blocks/surveys/components/sending-domains.tsx +113 -0
- package/blocks/surveys/components/survey-form.tsx +126 -0
- package/blocks/surveys/components/survey-responses.tsx +145 -0
- package/blocks/surveys/components/survey-results.tsx +72 -0
- package/blocks/surveys/components/survey-send-history.tsx +58 -0
- package/blocks/surveys/components/survey-send.tsx +162 -0
- package/blocks/surveys/components/survey-triggers.tsx +166 -0
- package/blocks/surveys/components/surveys-screen.tsx +234 -0
- package/blocks/surveys/lib/surveys-types.ts +228 -0
- package/blocks/surveys/recipes/create-an-nps-survey.ts +22 -0
- package/blocks/surveys/recipes/send-a-survey-and-follow-it.ts +27 -0
- package/blocks/surveys/recipes/survey-score-report.ts +27 -0
- package/blocks/visitors/components/snippet-install.tsx +18 -9
- package/blocks/visitors/components/traffic-chart.tsx +7 -6
- package/blocks/visitors/components/visitor-company-card.tsx +24 -14
- package/blocks/visitors/components/visitor-feed.tsx +5 -5
- package/blocks/web-analytics/README.md +102 -0
- package/blocks/web-analytics/block.json +292 -0
- package/blocks/web-analytics/components/acquisition-tab.tsx +154 -0
- package/blocks/web-analytics/components/analytics-filters.tsx +161 -0
- package/blocks/web-analytics/components/audience-tab.tsx +135 -0
- package/blocks/web-analytics/components/conversions-tab.tsx +128 -0
- package/blocks/web-analytics/components/engagement-tab.tsx +123 -0
- package/blocks/web-analytics/components/overview-tab.tsx +102 -0
- package/blocks/web-analytics/components/pages-tab.tsx +148 -0
- package/blocks/web-analytics/components/ranked-table.tsx +103 -0
- package/blocks/web-analytics/components/realtime-tab.tsx +103 -0
- package/blocks/web-analytics/components/report-card.tsx +71 -0
- package/blocks/web-analytics/components/web-analytics-screen.tsx +90 -0
- package/blocks/web-analytics/hooks/use-report.ts +48 -0
- package/blocks/web-analytics/lib/analytics-scope.ts +155 -0
- package/blocks/web-analytics/lib/page-path.ts +14 -0
- package/blocks/web-analytics/lib/web-analytics-types.ts +187 -0
- package/blocks/web-analytics/recipes/best-landing-pages.ts +26 -0
- package/blocks/web-analytics/recipes/form-conversion-check.ts +19 -0
- package/blocks/web-analytics/recipes/weekly-traffic-report.ts +37 -0
- package/blocks/webchat/components/chat-widget-config.tsx +1 -1
- package/blocks/webchat/components/conversation-list.tsx +1 -1
- package/blocks/workbench/README.md +54 -2
- package/blocks/workbench/block.json +234 -5
- package/blocks/workbench/components/column-recipe-editor.tsx +1 -1
- package/blocks/workbench/components/run-progress.tsx +1 -1
- package/blocks/workbench/components/workbench-auto-enrich.tsx +88 -0
- package/blocks/workbench/components/workbench-automation.tsx +36 -0
- package/blocks/workbench/components/workbench-connections.tsx +136 -0
- package/blocks/workbench/components/workbench-crm-import.tsx +91 -0
- package/blocks/workbench/components/workbench-datafile-import.tsx +154 -0
- package/blocks/workbench/components/workbench-grid.tsx +71 -8
- package/blocks/workbench/components/workbench-pipelines.tsx +173 -0
- package/blocks/workbench/components/workbench-row-actions.tsx +8 -1
- package/blocks/workbench/components/workbench-schedule.tsx +191 -0
- package/blocks/workbench/components/workbench-screen.tsx +65 -22
- package/blocks/workbench/components/workbench-send-to-sequence.tsx +116 -0
- package/blocks/workbench/components/workbench-table-list.tsx +14 -2
- package/blocks/workbench/components/workbench-table-settings.tsx +53 -0
- package/blocks/workbench/components/workbench-triggers.tsx +178 -0
- package/blocks/workbench/hooks/use-connected-crms.ts +29 -0
- package/blocks/workbench/lib/api-status.ts +8 -0
- package/blocks/workbench/lib/automation-types.ts +257 -0
- package/blocks/workflows/components/workflow-editor.tsx +1 -1
- package/dist/cli.js +744 -394
- package/dist/index.browser.js +1 -1
- package/dist/index.browser.js.map +1 -1
- package/dist/index.browser.mjs +1 -1
- package/dist/index.browser.mjs.map +1 -1
- package/dist/index.d.mts +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +1 -1
- package/dist/index.mjs.map +1 -1
- package/dist/react.browser.js +1 -1
- package/dist/react.browser.js.map +1 -1
- package/dist/react.browser.mjs +1 -1
- package/dist/react.browser.mjs.map +1 -1
- package/dist/react.d.mts +2 -2
- package/dist/react.d.ts +2 -2
- package/dist/react.js +1 -1
- package/dist/react.js.map +1 -1
- package/dist/react.mjs +1 -1
- package/dist/react.mjs.map +1 -1
- package/dist/types/blocks/auth.d.ts +45 -0
- package/dist/types/blocks/brand.d.ts +11 -2
- package/dist/types/blocks/create.d.ts +11 -4
- package/dist/types/blocks/doctor.d.ts +1 -0
- package/dist/types/blocks/flows.d.ts +7 -1
- package/dist/types/blocks/orgBrand.d.ts +34 -0
- package/dist/types/cli.d.ts +3 -0
- package/dist/types/version.d.ts +2 -2
- package/docs/api/desk.d.ts +2 -2
- package/docs/api/desk.md +3 -3
- package/docs/api/forms.md +1 -1
- package/package.json +1 -1
- package/stacks/6sense/pages/companies.tsx.tmpl +21 -0
- package/stacks/6sense/pages/contacts.tsx.tmpl +21 -0
- package/stacks/6sense/pages/tasks.tsx.tmpl +19 -0
- package/stacks/6sense.json +37 -4
- package/stacks/apollo/pages/calls.tsx.tmpl +23 -0
- package/stacks/apollo/pages/campaigns.tsx.tmpl +27 -0
- package/stacks/apollo/pages/companies.tsx.tmpl +19 -36
- package/stacks/apollo/pages/contacts.tsx.tmpl +35 -95
- package/stacks/apollo/pages/deals.tsx.tmpl +17 -0
- package/stacks/apollo/pages/inbox-contact.tsx.tmpl +13 -0
- package/stacks/apollo/pages/lists.tsx.tmpl +24 -0
- package/stacks/apollo/pages/meetings.tsx.tmpl +13 -0
- package/stacks/apollo/pages/sequences.tsx.tmpl +18 -0
- package/stacks/apollo/pages/tasks.tsx.tmpl +20 -0
- package/stacks/apollo.json +68 -9
- package/stacks/calendly/pages/companies.tsx.tmpl +19 -0
- package/stacks/calendly/pages/contacts.tsx.tmpl +21 -0
- package/stacks/calendly/pages/meetings.tsx.tmpl +13 -0
- package/stacks/calendly/pages/tasks.tsx.tmpl +19 -0
- package/stacks/calendly.json +8 -4
- package/stacks/clay/pages/companies.tsx.tmpl +17 -1
- package/stacks/clay/pages/contacts.tsx.tmpl +14 -0
- package/stacks/clay/pages/lists.tsx.tmpl +24 -0
- package/stacks/clay/pages/tasks.tsx.tmpl +19 -0
- package/stacks/clay.json +16 -3
- package/stacks/gong/pages/calls.tsx.tmpl +23 -0
- package/stacks/gong/pages/companies.tsx.tmpl +22 -0
- package/stacks/gong/pages/contacts.tsx.tmpl +34 -0
- package/stacks/gong/pages/lists.tsx.tmpl +12 -0
- package/stacks/gong/pages/meetings.tsx.tmpl +13 -0
- package/stacks/gong/pages/tasks.tsx.tmpl +19 -0
- package/stacks/gong.json +23 -6
- package/stacks/hootsuite/pages/companies.tsx.tmpl +22 -0
- package/stacks/hootsuite/pages/contacts.tsx.tmpl +30 -0
- package/stacks/hootsuite/pages/lists.tsx.tmpl +12 -0
- package/stacks/hootsuite/pages/tasks.tsx.tmpl +19 -0
- package/stacks/hootsuite.json +19 -4
- package/stacks/hubspot/pages/campaigns.tsx.tmpl +30 -0
- package/stacks/hubspot/pages/companies.tsx.tmpl +16 -35
- package/stacks/hubspot/pages/contacts.tsx.tmpl +35 -95
- package/stacks/hubspot/pages/deals.tsx.tmpl +14 -1
- package/stacks/hubspot/pages/forms.tsx.tmpl +36 -0
- package/stacks/hubspot/pages/inbox.tsx.tmpl +12 -1
- package/stacks/hubspot/pages/lifecycle.tsx.tmpl +23 -0
- package/stacks/hubspot/pages/lists.tsx.tmpl +12 -0
- package/stacks/hubspot/pages/meetings.tsx.tmpl +13 -0
- package/stacks/hubspot/pages/sequences.tsx.tmpl +18 -0
- package/stacks/hubspot/pages/tasks.tsx.tmpl +20 -0
- package/stacks/hubspot/pages/web-analytics.tsx.tmpl +14 -0
- package/stacks/hubspot.json +66 -7
- package/stacks/intercom/pages/companies.tsx.tmpl +19 -0
- package/stacks/intercom/pages/contacts.tsx.tmpl +21 -0
- package/stacks/intercom/pages/inbox-contact.tsx.tmpl +13 -0
- package/stacks/intercom/pages/tasks.tsx.tmpl +19 -0
- package/stacks/intercom.json +18 -4
- package/stacks/mailchimp/pages/companies.tsx.tmpl +22 -0
- package/stacks/mailchimp/pages/contacts.tsx.tmpl +30 -0
- package/stacks/mailchimp/pages/forms.tsx.tmpl +36 -0
- package/stacks/mailchimp/pages/lists.tsx.tmpl +12 -0
- package/stacks/mailchimp/pages/tasks.tsx.tmpl +19 -0
- package/stacks/mailchimp.json +22 -6
- package/stacks/outreach/pages/calls.tsx.tmpl +23 -0
- package/stacks/outreach/pages/campaigns.tsx.tmpl +27 -0
- package/stacks/outreach/pages/companies.tsx.tmpl +22 -0
- package/stacks/outreach/pages/contacts.tsx.tmpl +29 -66
- package/stacks/outreach/pages/inbox-contact.tsx.tmpl +13 -0
- package/stacks/outreach/pages/lists.tsx.tmpl +12 -0
- package/stacks/outreach/pages/meetings.tsx.tmpl +13 -0
- package/stacks/outreach/pages/sequences.tsx.tmpl +18 -0
- package/stacks/outreach/pages/tasks.tsx.tmpl +19 -0
- package/stacks/outreach.json +38 -9
- package/stacks/pipedrive/pages/companies.tsx.tmpl +11 -35
- package/stacks/pipedrive/pages/contacts.tsx.tmpl +17 -35
- package/stacks/pipedrive/pages/deals.tsx.tmpl +10 -1
- package/stacks/pipedrive/pages/inbox.tsx.tmpl +12 -1
- package/stacks/pipedrive/pages/meetings.tsx.tmpl +13 -0
- package/stacks/pipedrive/pages/tasks.tsx.tmpl +20 -0
- package/stacks/pipedrive.json +14 -2
- package/stacks/salesforce/pages/companies.tsx.tmpl +16 -35
- package/stacks/salesforce/pages/contacts.tsx.tmpl +26 -35
- package/stacks/salesforce/pages/deals.tsx.tmpl +14 -1
- package/stacks/salesforce/pages/lifecycle.tsx.tmpl +23 -0
- package/stacks/salesforce/pages/lists.tsx.tmpl +12 -0
- package/stacks/salesforce/pages/tasks.tsx.tmpl +20 -0
- package/stacks/salesforce.json +17 -3
- package/stacks/zoominfo/pages/companies.tsx.tmpl +17 -1
- package/stacks/zoominfo/pages/contacts.tsx.tmpl +16 -0
- package/stacks/zoominfo/pages/lists.tsx.tmpl +24 -0
- package/stacks/zoominfo/pages/tasks.tsx.tmpl +19 -0
- package/stacks/zoominfo.json +36 -3
- package/styles/styles.css +1 -1
- package/templates/auth/auth-route.ts.tmpl +16 -0
- package/templates/auth/g8.auth.anonymous.ts.tmpl +15 -0
- package/templates/auth/g8.auth.basic.ts.tmpl +17 -0
- package/templates/auth/g8.auth.jwt.ts.tmpl +23 -0
- package/templates/auth/g8.auth.password.ts.tmpl +18 -0
- package/templates/auth/g8.auth.session.ts.tmpl +23 -0
- package/templates/auth/middleware.ts.tmpl +14 -0
- package/templates/next-app/README.md.tmpl +5 -0
- package/templates/next-app/app/globals.css.tmpl +349 -64
- package/templates/next-app/components/app-shell.tsx.tmpl +6 -0
- package/templates/next-app/components/shell/quick-jump.tsx.tmpl +4 -0
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* PageClone: turn an existing web page into an editable landing page, from its public URL or from HTML you paste.
|
|
5
|
+
*
|
|
6
|
+
* SDK operations, in the order the screen uses them:
|
|
7
|
+
* 1. cloneUrlToLandingPage POST /landing-pages/clone-url { url, name?, use_vision } (BILLABLE, confirmed)
|
|
8
|
+
* 2. cloneHtmlToLandingPage POST /landing-pages/clone-html { html, name?, source_url?, use_vision } (BILLABLE, confirmed)
|
|
9
|
+
*
|
|
10
|
+
* Both run AI passes; "match the look from a screenshot" is the expensive half and is on by default, as in the API.
|
|
11
|
+
* The API quotes no price in advance and the balance is not checked here, so the dialog says what runs. The new page
|
|
12
|
+
* is a draft: nothing is published. Pasted HTML is limited to 4 MiB (the proxy rejects more than 5 MiB).
|
|
13
|
+
*/
|
|
14
|
+
import { useState } from "react";
|
|
15
|
+
import { Button, Input, Label, Textarea } from "@graph8/sdk/ui";
|
|
16
|
+
import { ApiError } from "@/g8/components/api-error";
|
|
17
|
+
import { ConfirmSpendDialog } from "@/g8/components/confirm-spend-dialog";
|
|
18
|
+
import { useConfirmAction } from "@/g8/components/confirmed-action";
|
|
19
|
+
import { FormSheet } from "@/g8/components/screen/form-sheet";
|
|
20
|
+
import { g8Post } from "@/g8/lib/g8-client";
|
|
21
|
+
import { asRec, text } from "../lib/studio";
|
|
22
|
+
|
|
23
|
+
export interface PageCloneProps {
|
|
24
|
+
/** Called with the new page's id once the clone is made. */
|
|
25
|
+
onCreated?: (pageId: string) => void;
|
|
26
|
+
/** The button label. */
|
|
27
|
+
label?: string;
|
|
28
|
+
className?: string;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export const MAX_HTML_BYTES = 4 * 1024 * 1024;
|
|
32
|
+
|
|
33
|
+
/** True for an http(s) URL with a host: what the clone endpoint can fetch. */
|
|
34
|
+
export function isPublicUrl(raw: string): boolean {
|
|
35
|
+
try {
|
|
36
|
+
const u = new URL(raw.trim());
|
|
37
|
+
return (u.protocol === "http:" || u.protocol === "https:") && u.hostname.includes(".");
|
|
38
|
+
} catch {
|
|
39
|
+
return false;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function PageClone({ onCreated, label = "Clone a page", className }: PageCloneProps) {
|
|
44
|
+
const { ask, dialog, failure } = useConfirmAction();
|
|
45
|
+
const [open, setOpen] = useState(false);
|
|
46
|
+
const [mode, setMode] = useState<"url" | "html">("url");
|
|
47
|
+
const [url, setUrl] = useState("");
|
|
48
|
+
const [html, setHtml] = useState("");
|
|
49
|
+
const [name, setName] = useState("");
|
|
50
|
+
const [vision, setVision] = useState(true);
|
|
51
|
+
const htmlBytes = new TextEncoder().encode(html).length;
|
|
52
|
+
const ready = mode === "url" ? isPublicUrl(url) : html.trim() !== "" && htmlBytes <= MAX_HTML_BYTES;
|
|
53
|
+
|
|
54
|
+
const reset = (): void => { setUrl(""); setHtml(""); setName(""); setVision(true); };
|
|
55
|
+
const go = (): void => ask({
|
|
56
|
+
tier: "billable",
|
|
57
|
+
title: mode === "url" ? `Clone ${url.trim()} into an editable page` : "Clone the pasted HTML into an editable page",
|
|
58
|
+
confirmLabel: "Clone page",
|
|
59
|
+
description: `Runs AI passes to rebuild the page as editable sections${vision ? ", including a screenshot pass so it looks like the original" : ""}. Uses credits; the API gives no price in advance and your balance is not checked here. The new page is a draft.`,
|
|
60
|
+
run: async () => {
|
|
61
|
+
const common = { name: name.trim() || undefined, use_vision: vision };
|
|
62
|
+
const res = mode === "url" ? await g8Post("/landing-pages/clone-url", { url: url.trim(), ...common }) : await g8Post("/landing-pages/clone-html", { html, ...common });
|
|
63
|
+
const id = text(asRec(res.data).id);
|
|
64
|
+
reset();
|
|
65
|
+
setOpen(false);
|
|
66
|
+
if (id) onCreated?.(id);
|
|
67
|
+
},
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
return (
|
|
71
|
+
<div className={className}>
|
|
72
|
+
<Button size="sm" variant="outline" onClick={() => setOpen(true)}>{label}</Button>
|
|
73
|
+
<FormSheet
|
|
74
|
+
open={open}
|
|
75
|
+
onOpenChange={setOpen}
|
|
76
|
+
title="Clone a page"
|
|
77
|
+
description="Rebuild a page you already have as an editable landing page. It starts as a draft."
|
|
78
|
+
width="lg"
|
|
79
|
+
footer={<><Button variant="outline" onClick={() => setOpen(false)}>Cancel</Button><Button disabled={!ready} onClick={go}>Clone page</Button></>}
|
|
80
|
+
>
|
|
81
|
+
<div className="flex gap-1" role="tablist" aria-label="Clone source">
|
|
82
|
+
<Button size="sm" role="tab" aria-selected={mode === "url"} variant={mode === "url" ? "default" : "outline"} onClick={() => setMode("url")}>From a URL</Button>
|
|
83
|
+
<Button size="sm" role="tab" aria-selected={mode === "html"} variant={mode === "html" ? "default" : "outline"} onClick={() => setMode("html")}>From HTML</Button>
|
|
84
|
+
</div>
|
|
85
|
+
{mode === "url" ? (
|
|
86
|
+
<div><Label htmlFor="pc-url" className="mb-1">Page URL</Label><Input id="pc-url" type="url" placeholder="https://example.com/pricing" value={url} onChange={(e) => setUrl(e.target.value)} aria-invalid={url !== "" && !isPublicUrl(url)} />
|
|
87
|
+
{url !== "" && !isPublicUrl(url) ? <p role="alert" className="text-error-foreground mt-1 text-xs">Enter a full address starting with https://.</p> : null}</div>
|
|
88
|
+
) : (
|
|
89
|
+
<div><Label htmlFor="pc-html" className="mb-1">HTML</Label><Textarea id="pc-html" rows={10} className="font-mono text-xs" value={html} onChange={(e) => setHtml(e.target.value)} />
|
|
90
|
+
{htmlBytes > MAX_HTML_BYTES ? <p role="alert" className="text-error-foreground mt-1 text-xs">This is {(htmlBytes / 1048576).toFixed(1)} MiB. The limit is 4 MiB.</p> : null}</div>
|
|
91
|
+
)}
|
|
92
|
+
<div><Label htmlFor="pc-name" className="mb-1">Name (optional)</Label><Input id="pc-name" value={name} onChange={(e) => setName(e.target.value)} /></div>
|
|
93
|
+
<label className="flex items-center gap-2 text-sm"><input type="checkbox" checked={vision} onChange={(e) => setVision(e.target.checked)} />Match the look from a screenshot (uses more credits)</label>
|
|
94
|
+
{failure ? <ApiError error={failure} title="The clone did not work" /> : null}
|
|
95
|
+
</FormSheet>
|
|
96
|
+
<ConfirmSpendDialog {...dialog} />
|
|
97
|
+
</div>
|
|
98
|
+
);
|
|
99
|
+
}
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* PageDomains: where one landing page is live. Lists every URL the page is bound to, takes one off the internet
|
|
5
|
+
* without unpublishing the page, and publishes the page to another of your verified domains.
|
|
6
|
+
*
|
|
7
|
+
* SDK operations, in the order the screen uses them:
|
|
8
|
+
* 1. listLandingPageBindings GET /landing-pages/{id}/bindings (read)
|
|
9
|
+
* 2. listLandingPageDomains GET /landing-pages/domains (read; the domains a page can be published on)
|
|
10
|
+
* 3. publishLandingPage POST /landing-pages/{id}/publish { domain_id, subdomain, path } (external, confirmed: a public URL)
|
|
11
|
+
* 4. removeLandingPageBinding DELETE /landing-pages/{id}/bindings?hostname&path (destructive, confirmed: that URL stops resolving)
|
|
12
|
+
* 5. exportLandingPage GET /landing-pages/{id}/export (read; downloads the page as one self-contained HTML file)
|
|
13
|
+
*
|
|
14
|
+
* Publishing to a domain needs the domain to be connected for hosting (the shared default domain is published from the
|
|
15
|
+
* Edit tab). A domain that is not ready is listed but cannot be chosen, with the reason. A URL already used by another
|
|
16
|
+
* page is refused by the API; nothing is taken over from here.
|
|
17
|
+
*/
|
|
18
|
+
import { useState } from "react";
|
|
19
|
+
import { Download } from "lucide-react";
|
|
20
|
+
import { Button, Input, Label, Skeleton } from "@graph8/sdk/ui";
|
|
21
|
+
import { ApiError } from "@/g8/components/api-error";
|
|
22
|
+
import { ConfirmSpendDialog } from "@/g8/components/confirm-spend-dialog";
|
|
23
|
+
import { useConfirmAction } from "@/g8/components/confirmed-action";
|
|
24
|
+
import { EmptyState } from "@/g8/components/empty-state";
|
|
25
|
+
import { SectionCard } from "@/g8/components/screen/section-card";
|
|
26
|
+
import { useG8Query } from "@/g8/hooks/use-g8-query";
|
|
27
|
+
import { useG8Mutation } from "@/g8/hooks/use-g8-mutation";
|
|
28
|
+
import { g8Delete, g8Get, g8Post } from "@/g8/lib/g8-client";
|
|
29
|
+
import { asRec, num, rowsOf, shortDate, text } from "../lib/studio";
|
|
30
|
+
import type { Rec } from "../lib/studio";
|
|
31
|
+
|
|
32
|
+
export interface PageDomainsProps {
|
|
33
|
+
pageId: string;
|
|
34
|
+
/** Called after a URL was added or removed, so the page list and status refresh. */
|
|
35
|
+
onChanged?: () => void;
|
|
36
|
+
className?: string;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const SELECT = "border-input bg-background h-9 rounded-md border px-2 text-sm";
|
|
40
|
+
|
|
41
|
+
/** The slash-led path the API stores: "" and "/" are the root. */
|
|
42
|
+
export function normalizePath(raw: string): string {
|
|
43
|
+
const p = raw.trim().replace(/\/+$/, "");
|
|
44
|
+
return p ? (p.startsWith("/") ? p : `/${p}`) : "/";
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** A subdomain label: lowercase letters, digits and hyphens, not starting or ending with a hyphen. Empty means the apex. */
|
|
48
|
+
export function validSubdomain(raw: string): boolean {
|
|
49
|
+
const s = raw.trim();
|
|
50
|
+
return s === "" || s === "@" || /^[a-z0-9]([a-z0-9-]*[a-z0-9])?$/i.test(s);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function PageDomains({ pageId, onChanged, className }: PageDomainsProps) {
|
|
54
|
+
const base = `/landing-pages/${encodeURIComponent(pageId)}`;
|
|
55
|
+
const { ask, dialog, failure } = useConfirmAction();
|
|
56
|
+
const bindings = useG8Query((signal) => g8Get(`${base}/bindings`, undefined, { signal }), [pageId]);
|
|
57
|
+
const domains = useG8Query((signal) => g8Get("/landing-pages/domains", undefined, { signal }), []);
|
|
58
|
+
const [domainId, setDomainId] = useState("");
|
|
59
|
+
const [subdomain, setSubdomain] = useState("");
|
|
60
|
+
const [path, setPath] = useState("");
|
|
61
|
+
|
|
62
|
+
const exportHtml = useG8Mutation(async () => {
|
|
63
|
+
const d = asRec((await g8Get(`${base}/export`)).data);
|
|
64
|
+
const html = text(d.html) || text(d.html_output);
|
|
65
|
+
if (!html) throw new Error("This page has no HTML to download yet.");
|
|
66
|
+
const url = URL.createObjectURL(new Blob([html], { type: "text/html" }));
|
|
67
|
+
const a = document.createElement("a");
|
|
68
|
+
a.href = url;
|
|
69
|
+
a.download = `${(text(d.name) || pageId).replace(/[^a-z0-9-_]+/gi, "-").toLowerCase()}.html`;
|
|
70
|
+
a.click();
|
|
71
|
+
URL.revokeObjectURL(url);
|
|
72
|
+
});
|
|
73
|
+
const changed = (): void => { void bindings.refetch(); onChanged?.(); };
|
|
74
|
+
const rows = rowsOf<Rec>(bindings.data?.data, "bindings");
|
|
75
|
+
const primary = asRec(asRec(bindings.data?.data).primary);
|
|
76
|
+
const available = rowsOf<Rec>(domains.data?.data, "domains");
|
|
77
|
+
const chosen = available.find((d) => String(d.id) === domainId);
|
|
78
|
+
const publishable = available.filter((d) => num(d.id) != null && d.inboxkit_connected === true);
|
|
79
|
+
const formOk = Boolean(chosen) && validSubdomain(subdomain);
|
|
80
|
+
|
|
81
|
+
const publish = (): void => {
|
|
82
|
+
if (!chosen) return;
|
|
83
|
+
const host = [subdomain.trim() && subdomain.trim() !== "@" ? subdomain.trim() : "", text(chosen.domain)].filter(Boolean).join(".");
|
|
84
|
+
const url = `https://${host}${normalizePath(path)}`;
|
|
85
|
+
ask({
|
|
86
|
+
tier: "external",
|
|
87
|
+
title: `Publish this page at ${url}`,
|
|
88
|
+
confirmLabel: "Publish",
|
|
89
|
+
description: "The page goes live at this address and anyone with the link can see it. A URL already used by another page is refused, nothing is replaced.",
|
|
90
|
+
run: () => g8Post(`${base}/publish`, { domain_id: Number(chosen.id), subdomain: subdomain.trim() === "@" ? "" : subdomain.trim(), path: normalizePath(path) }),
|
|
91
|
+
onDone: () => { setSubdomain(""); setPath(""); changed(); },
|
|
92
|
+
});
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
const remove = (b: Rec): void => {
|
|
96
|
+
const hostname = text(b.hostname);
|
|
97
|
+
const bindingPath = text(b.path, "/");
|
|
98
|
+
ask({
|
|
99
|
+
tier: "destructive",
|
|
100
|
+
title: `Take down ${hostname}${bindingPath === "/" ? "" : bindingPath}`,
|
|
101
|
+
confirmLabel: "Remove URL",
|
|
102
|
+
affected: 1,
|
|
103
|
+
description: "This URL stops serving the page. Links to it in ads and emails will 404. The page and its other URLs stay; DNS records are not touched.",
|
|
104
|
+
tolerateStatus: [404],
|
|
105
|
+
run: () => g8Delete(`${base}/bindings`, { query: { hostname, path: bindingPath } }),
|
|
106
|
+
onDone: changed,
|
|
107
|
+
});
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
return (
|
|
111
|
+
<div className={className}>
|
|
112
|
+
<div className="grid gap-4">
|
|
113
|
+
<SectionCard title="Live URLs" description="Every address that serves this page."
|
|
114
|
+
actions={<Button size="sm" variant="outline" loading={exportHtml.loading} onClick={() => void exportHtml.mutate(undefined)}><Download className="size-4" />Download HTML</Button>}>
|
|
115
|
+
{exportHtml.error ? <ApiError error={exportHtml.error} title="Could not download the page" /> : null}
|
|
116
|
+
{bindings.error ? <ApiError error={bindings.error} title="Could not load the URLs" onRetry={() => void bindings.refetch()} /> : bindings.loading && !bindings.data ? (
|
|
117
|
+
<Skeleton className="h-16 w-full" aria-busy="true" />
|
|
118
|
+
) : rows.length === 0 ? (
|
|
119
|
+
<EmptyState compact title="No custom URLs" description={text(primary.published_url) ? `Live at ${text(primary.published_url)}.` : "This page is not published to a custom domain yet."} />
|
|
120
|
+
) : (
|
|
121
|
+
<ul className="grid gap-2">
|
|
122
|
+
{rows.map((b) => (
|
|
123
|
+
<li key={`${text(b.hostname)}${text(b.path)}`} className="flex items-center justify-between gap-3 rounded-md border p-3">
|
|
124
|
+
<div className="min-w-0">
|
|
125
|
+
<span className="block truncate text-sm font-medium">{text(b.published_url) || `${text(b.hostname)}${text(b.path)}`}</span>
|
|
126
|
+
<span className="text-muted-foreground block text-xs">{b.published_at ? `Published ${shortDate(b.published_at)}` : "Published"}</span>
|
|
127
|
+
</div>
|
|
128
|
+
<Button size="sm" variant="ghost" aria-label={`Remove ${text(b.hostname)}`} onClick={() => remove(b)}>Remove</Button>
|
|
129
|
+
</li>
|
|
130
|
+
))}
|
|
131
|
+
</ul>
|
|
132
|
+
)}
|
|
133
|
+
</SectionCard>
|
|
134
|
+
|
|
135
|
+
<SectionCard title="Publish to a domain" description="Choose one of your connected domains, then an optional subdomain and path.">
|
|
136
|
+
{domains.error ? <ApiError error={domains.error} title="Could not load your domains" onRetry={() => void domains.refetch()} /> : domains.loading && !domains.data ? (
|
|
137
|
+
<Skeleton className="h-16 w-full" aria-busy="true" />
|
|
138
|
+
) : publishable.length === 0 ? (
|
|
139
|
+
<EmptyState compact title="No connected domains" description="Connect a domain for hosting first. The shared default domain is published from the Edit tab." />
|
|
140
|
+
) : (
|
|
141
|
+
<form className="grid gap-3 sm:grid-cols-3" onSubmit={(e) => { e.preventDefault(); if (formOk) publish(); }}>
|
|
142
|
+
<div>
|
|
143
|
+
<Label htmlFor="pd-domain" className="mb-1">Domain</Label>
|
|
144
|
+
<select id="pd-domain" className={`${SELECT} w-full`} value={domainId} onChange={(e) => setDomainId(e.target.value)}>
|
|
145
|
+
<option value="">Choose a domain</option>
|
|
146
|
+
{publishable.map((d) => <option key={String(d.id)} value={String(d.id)}>{text(d.domain)}</option>)}
|
|
147
|
+
</select>
|
|
148
|
+
</div>
|
|
149
|
+
<div>
|
|
150
|
+
<Label htmlFor="pd-sub" className="mb-1">Subdomain (optional)</Label>
|
|
151
|
+
<Input id="pd-sub" value={subdomain} placeholder="www or empty for the root" onChange={(e) => setSubdomain(e.target.value)} aria-invalid={!validSubdomain(subdomain)} />
|
|
152
|
+
</div>
|
|
153
|
+
<div>
|
|
154
|
+
<Label htmlFor="pd-path" className="mb-1">Path (optional)</Label>
|
|
155
|
+
<Input id="pd-path" value={path} placeholder="/pricing" onChange={(e) => setPath(e.target.value)} />
|
|
156
|
+
</div>
|
|
157
|
+
{!validSubdomain(subdomain) ? <p role="alert" className="text-error-foreground text-xs sm:col-span-3">A subdomain can only hold letters, numbers and hyphens.</p> : null}
|
|
158
|
+
<div className="sm:col-span-3"><Button type="submit" disabled={!formOk}>Publish to this domain</Button></div>
|
|
159
|
+
</form>
|
|
160
|
+
)}
|
|
161
|
+
</SectionCard>
|
|
162
|
+
{failure ? <ApiError error={failure} title="That did not work" /> : null}
|
|
163
|
+
</div>
|
|
164
|
+
<ConfirmSpendDialog {...dialog} />
|
|
165
|
+
</div>
|
|
166
|
+
);
|
|
167
|
+
}
|
|
@@ -187,7 +187,7 @@ export function PageEditor({ pageId, onSaved, className }: PageEditorProps) {
|
|
|
187
187
|
return (
|
|
188
188
|
<div className={className}>
|
|
189
189
|
<div className="mb-4 flex flex-wrap items-center gap-2">
|
|
190
|
-
<h2 className="text-lg font-
|
|
190
|
+
<h2 className="text-lg font-medium">{text(data.name, "Untitled page")}</h2>
|
|
191
191
|
<span className="text-muted-foreground text-sm">
|
|
192
192
|
{text(data.status, "draft")}
|
|
193
193
|
{url ? ` · ${url}` : ""}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* PageGenerationStatus: shows how far an AI-built page is while it is still being made, and calls `onDone` when it
|
|
5
|
+
* finishes. Renders nothing for a page that is not generating. A page that failed shows its error.
|
|
6
|
+
*
|
|
7
|
+
* SDK operations:
|
|
8
|
+
* 1. getLandingPageStatus GET /landing-pages/{id}/status (read; polled every 4 s only while the page is generating)
|
|
9
|
+
*/
|
|
10
|
+
import { useEffect, useRef } from "react";
|
|
11
|
+
import { ApiError } from "@/g8/components/api-error";
|
|
12
|
+
import { useG8Query } from "@/g8/hooks/use-g8-query";
|
|
13
|
+
import { g8Get } from "@/g8/lib/g8-client";
|
|
14
|
+
import { asRec, num, text } from "../lib/studio";
|
|
15
|
+
|
|
16
|
+
export interface PageGenerationStatusProps {
|
|
17
|
+
pageId: string;
|
|
18
|
+
/** The status the page row already shows. Polling starts only when it is an in-progress one. */
|
|
19
|
+
status?: string;
|
|
20
|
+
onDone?: () => void;
|
|
21
|
+
className?: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const WORKING = new Set(["generating", "pending", "processing", "queued", "in_progress"]);
|
|
25
|
+
export const isGenerating = (status: string | undefined): boolean => WORKING.has(String(status ?? "").toLowerCase());
|
|
26
|
+
|
|
27
|
+
export function PageGenerationStatus({ pageId, status, onDone, className }: PageGenerationStatusProps) {
|
|
28
|
+
const res = useG8Query((signal) => g8Get(`/landing-pages/${encodeURIComponent(pageId)}/status`, undefined, { signal }), [pageId, status], { enabled: isGenerating(status) });
|
|
29
|
+
const d = asRec(res.data?.data);
|
|
30
|
+
const live = isGenerating(text(d.status) || status);
|
|
31
|
+
// The status read can say "done" while the row still says "generating" (a worker died): tell the parent once per page, never in a loop.
|
|
32
|
+
const told = useRef<string | null>(null);
|
|
33
|
+
useEffect(() => {
|
|
34
|
+
if (!isGenerating(status)) return;
|
|
35
|
+
if (res.data && !live) { if (told.current !== pageId) { told.current = pageId; onDone?.(); } return; }
|
|
36
|
+
const t = setTimeout(() => void res.refetch(), 4000);
|
|
37
|
+
return () => clearTimeout(t);
|
|
38
|
+
}, [status, res.data, live]); // eslint-disable-line react-hooks/exhaustive-deps
|
|
39
|
+
if (!isGenerating(status)) return null;
|
|
40
|
+
if (res.error) return <ApiError error={res.error} title="Could not read the page status" onRetry={() => void res.refetch()} />;
|
|
41
|
+
const progress = num(d.progress);
|
|
42
|
+
return (
|
|
43
|
+
<p role="status" className={className ?? "text-sm"}>
|
|
44
|
+
Generating this page{progress != null ? `: ${progress}%` : ""}
|
|
45
|
+
{num(d.sections_completed) ? `, ${d.sections_completed} sections written` : ""}. This view updates by itself.
|
|
46
|
+
</p>
|
|
47
|
+
);
|
|
48
|
+
}
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* PageTracking: how one landing page is measured. Whether visits are recorded on the workspace stream, on a stream of
|
|
5
|
+
* its own or not at all, which form it reports, whether the chat widget shows, and what counts as a conversion.
|
|
6
|
+
* Changes apply the next time the page is published.
|
|
7
|
+
*
|
|
8
|
+
* SDK operations, in the order the screen uses them:
|
|
9
|
+
* 1. getLandingPageTracking GET /landing-pages/{id}/tracking (read)
|
|
10
|
+
* 2. updateLandingPageTracking PATCH /landing-pages/{id}/tracking (write; ONLY the fields you changed are sent)
|
|
11
|
+
* 3. provisionLandingPageTracking POST /landing-pages/{id}/tracking/provision (write; gives the page its own stream)
|
|
12
|
+
* 4. clearLandingPageConsentOverride DELETE /landing-pages/{id}/tracking/consent (destructive, confirmed: drops the page's own consent banner settings)
|
|
13
|
+
*
|
|
14
|
+
* The form is seeded from the loaded record only, and Save stays disabled until that record has arrived, so a blank
|
|
15
|
+
* form can never be written over real settings. "Dedicated" is reached through Provision (the API refuses a direct switch).
|
|
16
|
+
*/
|
|
17
|
+
import { useState } from "react";
|
|
18
|
+
import { Button, Input, Label, Skeleton, Switch } from "@graph8/sdk/ui";
|
|
19
|
+
import { ApiError } from "@/g8/components/api-error";
|
|
20
|
+
import { ConfirmSpendDialog } from "@/g8/components/confirm-spend-dialog";
|
|
21
|
+
import { useConfirmAction } from "@/g8/components/confirmed-action";
|
|
22
|
+
import { KeyValueList } from "@/g8/components/screen/key-value-list";
|
|
23
|
+
import { SectionCard } from "@/g8/components/screen/section-card";
|
|
24
|
+
import { useG8Mutation } from "@/g8/hooks/use-g8-mutation";
|
|
25
|
+
import { useG8Query } from "@/g8/hooks/use-g8-query";
|
|
26
|
+
import { g8Delete, g8Get, g8Patch, g8Post } from "@/g8/lib/g8-client";
|
|
27
|
+
import { asRec, shortDate, text } from "../lib/studio";
|
|
28
|
+
import type { Rec } from "../lib/studio";
|
|
29
|
+
|
|
30
|
+
export interface PageTrackingProps {
|
|
31
|
+
pageId: string;
|
|
32
|
+
onChanged?: () => void;
|
|
33
|
+
className?: string;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const SELECT = "border-input bg-background h-9 rounded-md border px-2 text-sm";
|
|
37
|
+
const MODES = [
|
|
38
|
+
{ value: "org_default", label: "Workspace stream (default)" },
|
|
39
|
+
{ value: "disabled", label: "Off (no visits recorded)" },
|
|
40
|
+
];
|
|
41
|
+
const GOALS = [
|
|
42
|
+
{ value: "", label: "Not set" },
|
|
43
|
+
{ value: "form_submission", label: "A form is submitted" },
|
|
44
|
+
{ value: "workspace_signup", label: "Someone signs up" },
|
|
45
|
+
{ value: "paid_account", label: "Someone pays" },
|
|
46
|
+
{ value: "traffic_only", label: "Traffic only" },
|
|
47
|
+
];
|
|
48
|
+
|
|
49
|
+
/** The fields that differ from the saved record: the body of the PATCH. Empty means nothing to save. */
|
|
50
|
+
export function trackingChanges(saved: Rec, draft: { mode: string; form_id: string; chat_enabled: boolean; conversion_goal: string }): Rec {
|
|
51
|
+
const out: Rec = {};
|
|
52
|
+
if (draft.mode !== text(saved.mode, "org_default") && draft.mode !== "dedicated") out.mode = draft.mode;
|
|
53
|
+
if (draft.form_id.trim() !== text(saved.form_id)) out.form_id = draft.form_id.trim();
|
|
54
|
+
if (draft.chat_enabled !== (saved.chat_enabled !== false)) out.chat_enabled = draft.chat_enabled;
|
|
55
|
+
if (draft.conversion_goal && draft.conversion_goal !== text(saved.conversion_goal)) out.conversion_goal = draft.conversion_goal;
|
|
56
|
+
return out;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function TrackingForm({ pageId, saved, onSaved }: { pageId: string; saved: Rec; onSaved: () => void }) {
|
|
60
|
+
const base = `/landing-pages/${encodeURIComponent(pageId)}/tracking`;
|
|
61
|
+
const { ask, dialog, failure } = useConfirmAction();
|
|
62
|
+
// Only what the person changed is held as `edits`, shown over the saved copy, so a reload never wipes typing.
|
|
63
|
+
const base0 = { mode: text(saved.mode, "org_default"), form_id: text(saved.form_id), chat_enabled: saved.chat_enabled !== false, conversion_goal: text(saved.conversion_goal) };
|
|
64
|
+
const [edits, setEdits] = useState<Partial<typeof base0>>({});
|
|
65
|
+
const draft = { ...base0, ...edits };
|
|
66
|
+
const setDraft = (next: typeof base0): void => setEdits(Object.fromEntries(Object.entries(next).filter(([k, v]) => v !== base0[k as keyof typeof base0])) as Partial<typeof base0>);
|
|
67
|
+
const changes = trackingChanges(saved, draft);
|
|
68
|
+
const dirty = Object.keys(changes).length > 0;
|
|
69
|
+
const save = useG8Mutation(() => g8Patch(base, changes), { onSuccess: () => { setEdits({}); onSaved(); } });
|
|
70
|
+
const provision = useG8Mutation(() => g8Post(`${base}/provision`), { onSuccess: onSaved });
|
|
71
|
+
const dedicated = saved.mode === "dedicated";
|
|
72
|
+
return (
|
|
73
|
+
<div className="grid gap-4">
|
|
74
|
+
<form className="grid gap-3 sm:grid-cols-2" onSubmit={(e) => { e.preventDefault(); if (dirty) void save.mutate(undefined); }}>
|
|
75
|
+
<div>
|
|
76
|
+
<Label htmlFor="pt-mode" className="mb-1">Measure visits</Label>
|
|
77
|
+
{dedicated ? <p className="text-sm">On a stream of its own</p> : (
|
|
78
|
+
<select id="pt-mode" className={`${SELECT} w-full`} value={draft.mode} onChange={(e) => setDraft({ ...draft, mode: e.target.value })}>
|
|
79
|
+
{MODES.map((m) => <option key={m.value} value={m.value}>{m.label}</option>)}
|
|
80
|
+
</select>
|
|
81
|
+
)}
|
|
82
|
+
</div>
|
|
83
|
+
<div>
|
|
84
|
+
<Label htmlFor="pt-goal" className="mb-1">A conversion is</Label>
|
|
85
|
+
<select id="pt-goal" className={`${SELECT} w-full`} value={draft.conversion_goal} onChange={(e) => setDraft({ ...draft, conversion_goal: e.target.value })}>
|
|
86
|
+
{GOALS.map((g) => <option key={g.value} value={g.value} disabled={g.value === "" && Boolean(saved.conversion_goal)}>{g.label}</option>)}
|
|
87
|
+
</select>
|
|
88
|
+
</div>
|
|
89
|
+
<div>
|
|
90
|
+
<Label htmlFor="pt-form" className="mb-1">Form label</Label>
|
|
91
|
+
<Input id="pt-form" value={draft.form_id} placeholder="demo-request" onChange={(e) => setDraft({ ...draft, form_id: e.target.value })} />
|
|
92
|
+
</div>
|
|
93
|
+
<div className="flex items-end gap-2">
|
|
94
|
+
<Switch id="pt-chat" checked={draft.chat_enabled} onCheckedChange={(v) => setDraft({ ...draft, chat_enabled: v })} aria-label="Show the chat widget" />
|
|
95
|
+
<Label htmlFor="pt-chat">Show the chat widget on this page</Label>
|
|
96
|
+
</div>
|
|
97
|
+
<div className="flex gap-2 sm:col-span-2">
|
|
98
|
+
<Button type="submit" loading={save.loading} disabled={!dirty}>Save tracking</Button>
|
|
99
|
+
{!dedicated ? <Button type="button" variant="outline" loading={provision.loading} onClick={() => void provision.mutate(undefined)}>Give this page its own stream</Button> : null}
|
|
100
|
+
</div>
|
|
101
|
+
</form>
|
|
102
|
+
{save.error ? <ApiError error={save.error} title="Could not save tracking" /> : null}
|
|
103
|
+
{save.data && !dirty ? <p role="status" className="text-sm">Tracking saved. It applies the next time the page is published.</p> : null}
|
|
104
|
+
{provision.error ? <ApiError error={provision.error} title="Could not set up the stream" /> : null}
|
|
105
|
+
<KeyValueList columns={2} items={[
|
|
106
|
+
{ label: "Stream", value: text(saved.stream_name) || "Workspace stream" },
|
|
107
|
+
{ label: "Provisioned", value: shortDate(saved.provisioned_at) },
|
|
108
|
+
{ label: "Page has a form", value: saved.has_forms === true ? "Yes" : saved.has_forms === false ? "No" : "" },
|
|
109
|
+
{ label: "Page has a sign-up", value: saved.has_graph8_signup === true ? "Yes" : saved.has_graph8_signup === false ? "No" : "" },
|
|
110
|
+
]} />
|
|
111
|
+
<div>
|
|
112
|
+
<Button size="sm" variant="ghost" onClick={() => ask({
|
|
113
|
+
tier: "destructive",
|
|
114
|
+
title: "Use the workspace consent banner on this page",
|
|
115
|
+
confirmLabel: "Use workspace setting",
|
|
116
|
+
description: "Drops this page's own consent banner settings. The page follows the workspace default the next time it is published.",
|
|
117
|
+
run: () => g8Delete(`${base}/consent`),
|
|
118
|
+
onDone: onSaved,
|
|
119
|
+
})}>Reset consent to the workspace default</Button>
|
|
120
|
+
</div>
|
|
121
|
+
{failure ? <ApiError error={failure} title="That did not work" /> : null}
|
|
122
|
+
<ConfirmSpendDialog {...dialog} />
|
|
123
|
+
</div>
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export function PageTracking({ pageId, onChanged, className }: PageTrackingProps) {
|
|
128
|
+
const res = useG8Query((signal) => g8Get(`/landing-pages/${encodeURIComponent(pageId)}/tracking`, undefined, { signal }), [pageId]);
|
|
129
|
+
const saved = asRec(res.data?.data);
|
|
130
|
+
return (
|
|
131
|
+
<SectionCard className={className} title="Tracking" description="Applies the next time the page is published.">
|
|
132
|
+
{res.error ? <ApiError error={res.error} title="Could not load tracking" onRetry={() => void res.refetch()} /> : !res.data ? <Skeleton className="h-32 w-full" aria-busy="true" /> : (
|
|
133
|
+
<TrackingForm key={pageId} pageId={pageId} saved={saved} onSaved={() => { void res.refetch(); onChanged?.(); }} />
|
|
134
|
+
)}
|
|
135
|
+
</SectionCard>
|
|
136
|
+
);
|
|
137
|
+
}
|
|
@@ -5,7 +5,9 @@
|
|
|
5
5
|
*
|
|
6
6
|
* Composes: ScreenLayout (title, tabs, stats), SplitView + DetailPanel (landing page list and the selected
|
|
7
7
|
* page's editor, deep link `?id=`), PageList (list, filter, blank or AI create, publish, unpublish, delete),
|
|
8
|
-
* PageEditor (details, sections, preview, versions, publish),
|
|
8
|
+
* PageEditor (details, sections, preview, versions, publish), PageDomains (live URLs, publish to a domain, download),
|
|
9
|
+
* PageTracking, PageAiTools (edit, review, search metadata), PageAnalytics (portfolio tab and per page), PageClone,
|
|
10
|
+
* ContentLibrary, BrandKitEditor, SeoReport.
|
|
9
11
|
*
|
|
10
12
|
* Every record is deep-linkable: `?id=` opens a landing page (Edit and Details tabs) or, on the Content tab
|
|
11
13
|
* (`?tab=content`), a content item with all of its fields and actions; the Brand kit tab is the one brand record.
|
|
@@ -16,30 +18,43 @@
|
|
|
16
18
|
* (confirmed); manage the content inventory (add by URL, edit, SEO enrichment, delete); re-scan the brand kit and
|
|
17
19
|
* choose page layouts; run an SEO audit and track keywords. The editor changes text fields of sections-mode pages;
|
|
18
20
|
* freeform HTML pages can be previewed, published and versioned, and their HTML is edited in the graph8 app.
|
|
21
|
+
* Every page also has Publishing (its URLs, publish to a connected domain, remove a URL, download the HTML), Tracking,
|
|
22
|
+
* AI tools (edit by instruction, review and apply fixes, write search metadata: each AI pass is a confirmed spend) and
|
|
23
|
+
* Analytics tabs; "Clone a page" rebuilds a URL or pasted HTML as a draft (confirmed spend); the Analytics tab shows
|
|
24
|
+
* visits, people and companies across all pages.
|
|
19
25
|
*
|
|
20
|
-
* SDK operations: listLandingPages (
|
|
21
|
-
*
|
|
26
|
+
* SDK operations: listLandingPages (the headline numbers read the API's totals, so they stay right past one page),
|
|
27
|
+
* getLandingPage (the title of a page that is not in the loaded list); everything else lives in the composed parts,
|
|
28
|
+
* which confirm every publish, unpublish, delete, URL removal, AI pass and paid audit.
|
|
22
29
|
*/
|
|
23
30
|
|
|
24
|
-
import { useState } from "react";
|
|
31
|
+
import { useRef, useState } from "react";
|
|
25
32
|
import type { ReactNode } from "react";
|
|
26
33
|
import { Globe } from "lucide-react";
|
|
27
34
|
import { Skeleton } from "@graph8/sdk/ui";
|
|
28
35
|
import { ApiError } from "@/g8/components/api-error";
|
|
36
|
+
import { FirstNotice } from "@/g8/components/paging-notices";
|
|
29
37
|
import { DetailPanel } from "@/g8/components/screen/detail-panel";
|
|
30
38
|
import { KeyValueList } from "@/g8/components/screen/key-value-list";
|
|
31
39
|
import { ScreenLayout } from "@/g8/components/screen/screen-layout";
|
|
32
40
|
import { SplitView } from "@/g8/components/screen/split-view";
|
|
33
41
|
import { useUrlState } from "@/g8/hooks/use-url-state";
|
|
34
42
|
import { g8Get } from "@/g8/lib/g8-client";
|
|
43
|
+
import { useAllPages } from "@/g8/hooks/use-all-pages";
|
|
35
44
|
import { useG8Query } from "@/g8/hooks/use-g8-query";
|
|
36
45
|
import { StatusPill } from "@/g8/components/screen/status-pill";
|
|
37
46
|
import { asRec, num, rowsOf, shortDate, text } from "../lib/studio";
|
|
38
47
|
import type { LandingPageRow } from "../lib/studio";
|
|
39
48
|
import { BrandKitEditor } from "./brand-kit-editor";
|
|
40
49
|
import { ContentLibrary } from "./content-library";
|
|
50
|
+
import { PageAiTools } from "./page-ai-tools";
|
|
51
|
+
import { PageAnalytics } from "./page-analytics";
|
|
52
|
+
import { PageClone } from "./page-clone";
|
|
53
|
+
import { PageDomains } from "./page-domains";
|
|
41
54
|
import { PageEditor } from "./page-editor";
|
|
55
|
+
import { PageGenerationStatus } from "./page-generation-status";
|
|
42
56
|
import { PageList } from "./page-list";
|
|
57
|
+
import { PageTracking } from "./page-tracking";
|
|
43
58
|
import { SeoReport } from "./seo-report";
|
|
44
59
|
|
|
45
60
|
export interface PagesScreenProps {
|
|
@@ -51,10 +66,10 @@ export interface PagesScreenProps {
|
|
|
51
66
|
className?: string;
|
|
52
67
|
}
|
|
53
68
|
|
|
54
|
-
type Tab = "pages" | "content" | "brand" | "seo";
|
|
69
|
+
type Tab = "pages" | "content" | "brand" | "seo" | "analytics";
|
|
55
70
|
|
|
56
71
|
/** Every field of one landing page (name and slug are edited in the Edit tab). */
|
|
57
|
-
function PageDetails({ pageId }: { pageId: string }) {
|
|
72
|
+
function PageDetails({ pageId, onGenerated }: { pageId: string; onGenerated?: () => void }) {
|
|
58
73
|
const res = useG8Query((signal) => g8Get(`/landing-pages/${encodeURIComponent(pageId)}`, undefined, { signal }), [pageId]);
|
|
59
74
|
if (res.error) return <ApiError error={res.error} title="Could not load the page" onRetry={() => void res.refetch()} />;
|
|
60
75
|
if (!res.data) return <Skeleton className="h-32 w-full" aria-busy="true" />;
|
|
@@ -62,6 +77,8 @@ function PageDetails({ pageId }: { pageId: string }) {
|
|
|
62
77
|
const n = (k: string): string => (d[k] == null || d[k] === "" ? "Not set" : text(d[k]));
|
|
63
78
|
const sections = Array.isArray(d.sections) ? d.sections.length : null;
|
|
64
79
|
return (
|
|
80
|
+
<div className="grid gap-3">
|
|
81
|
+
<PageGenerationStatus pageId={pageId} status={text(d.status)} onDone={onGenerated} />
|
|
65
82
|
<KeyValueList
|
|
66
83
|
columns={2}
|
|
67
84
|
items={[
|
|
@@ -78,23 +95,40 @@ function PageDetails({ pageId }: { pageId: string }) {
|
|
|
78
95
|
...(d.error_message ? [{ label: "Last error", value: text(d.error_message) }] : []),
|
|
79
96
|
]}
|
|
80
97
|
/>
|
|
98
|
+
</div>
|
|
81
99
|
);
|
|
82
100
|
}
|
|
83
101
|
|
|
102
|
+
/** Landing pages per read for the counts; the list itself pages through PageList. */
|
|
103
|
+
const LANDING_PAGE_SIZE = 100;
|
|
104
|
+
|
|
84
105
|
export function PagesScreen({ defaultDomain, renderDetailActions, extraDetailTabs, toolbarActions, className }: PagesScreenProps) {
|
|
85
106
|
const [tabParam, setTabParam] = useUrlState("tab", "pages");
|
|
86
|
-
const tab = (["pages", "content", "brand", "seo"].includes(tabParam) ? tabParam : "pages") as Tab;
|
|
107
|
+
const tab = (["pages", "content", "brand", "seo", "analytics"].includes(tabParam) ? tabParam : "pages") as Tab;
|
|
87
108
|
const [pageId, setPageId] = useUrlState("id", "");
|
|
88
109
|
const [view, setView] = useUrlState("view", "");
|
|
89
110
|
const expandProps = { onExpand: () => setView("full"), onCollapse: view === "full" ? () => setView(null) : undefined };
|
|
90
111
|
// The list owns its own fetch; bumping this remounts it after the editor changed something (publish, rename).
|
|
91
112
|
const [rev, setRev] = useState(0);
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
const
|
|
95
|
-
const
|
|
113
|
+
// The stat strip and the open page's name read EVERY landing page (100 per read, offset paged), so the counts are
|
|
114
|
+
// never those of a capped first read. `total` is the API's own count.
|
|
115
|
+
const apiTotal = useRef<number | undefined>(undefined);
|
|
116
|
+
const pages = useAllPages<LandingPageRow>(async (page, signal) => {
|
|
117
|
+
const offset = (page - 1) * LANDING_PAGE_SIZE;
|
|
118
|
+
const data = asRec((await g8Get("/landing-pages", { limit: LANDING_PAGE_SIZE, offset: offset || undefined }, { signal })).data);
|
|
119
|
+
const rows = rowsOf<LandingPageRow>(data, "landing_pages");
|
|
120
|
+
apiTotal.current = num(data.total) ?? apiTotal.current;
|
|
121
|
+
return { rows, hasNext: apiTotal.current != null ? offset + rows.length < apiTotal.current : rows.length >= LANDING_PAGE_SIZE };
|
|
122
|
+
}, [rev]);
|
|
123
|
+
const rows = pages.items;
|
|
124
|
+
const total = apiTotal.current ?? rows.length;
|
|
96
125
|
const published = rows.filter((p) => p.status === "published").length;
|
|
97
126
|
const current = tab === "pages" ? rows.find((p) => p.id === pageId) : undefined;
|
|
127
|
+
const bump = (): void => setRev((r) => r + 1);
|
|
128
|
+
// Pages whose generation "finished" per /status: held HERE, above the child that remounts on every rev, so a page the list still
|
|
129
|
+
// calls generating (a dead worker) refreshes the list once instead of looping.
|
|
130
|
+
const generatedOnce = useRef<Set<string>>(new Set());
|
|
131
|
+
const onGenerated = (id: string): void => { if (generatedOnce.current.has(id)) return; generatedOnce.current.add(id); bump(); };
|
|
98
132
|
|
|
99
133
|
const detail = tab === "pages" && pageId ? (
|
|
100
134
|
<DetailPanel
|
|
@@ -106,7 +140,11 @@ export function PagesScreen({ defaultDomain, renderDetailActions, extraDetailTab
|
|
|
106
140
|
{...expandProps}
|
|
107
141
|
tabs={[
|
|
108
142
|
{ id: "edit", label: "Edit", content: <PageEditor key={pageId} pageId={pageId} onSaved={() => setRev((r) => r + 1)} /> },
|
|
109
|
-
{ id: "details", label: "Details", content: <PageDetails key={`${pageId}:${rev}`} pageId={pageId} /> },
|
|
143
|
+
{ id: "details", label: "Details", content: <PageDetails key={`${pageId}:${rev}`} pageId={pageId} onGenerated={() => onGenerated(pageId)} /> },
|
|
144
|
+
{ id: "publishing", label: "Publishing", content: <PageDomains key={pageId} pageId={pageId} onChanged={bump} /> },
|
|
145
|
+
{ id: "tracking", label: "Tracking", content: <PageTracking key={pageId} pageId={pageId} onChanged={bump} /> },
|
|
146
|
+
{ id: "ai", label: "AI tools", content: <PageAiTools key={pageId} pageId={pageId} onChanged={bump} /> },
|
|
147
|
+
{ id: "analytics", label: "Analytics", content: <PageAnalytics key={pageId} pageId={pageId} /> },
|
|
110
148
|
...(extraDetailTabs ?? []).map((t) => ({ id: t.id, label: t.label, content: t.render(pageId) })),
|
|
111
149
|
]}
|
|
112
150
|
/>
|
|
@@ -118,9 +156,10 @@ export function PagesScreen({ defaultDomain, renderDetailActions, extraDetailTab
|
|
|
118
156
|
title="Pages"
|
|
119
157
|
description="Landing pages, your content library, the brand kit and SEO in one place."
|
|
120
158
|
icon={<Globe className="size-5" />}
|
|
121
|
-
actions={toolbarActions}
|
|
159
|
+
actions={<>{tab === "pages" ? <PageClone onCreated={(id) => { bump(); setPageId(id); }} /> : null}{toolbarActions}</>}
|
|
122
160
|
tabs={[
|
|
123
161
|
{ id: "pages", label: "Landing pages", count: total },
|
|
162
|
+
{ id: "analytics", label: "Analytics" },
|
|
124
163
|
{ id: "content", label: "Content" },
|
|
125
164
|
{ id: "brand", label: "Brand kit" },
|
|
126
165
|
{ id: "seo", label: "SEO" },
|
|
@@ -130,12 +169,13 @@ export function PagesScreen({ defaultDomain, renderDetailActions, extraDetailTab
|
|
|
130
169
|
stats={tab === "pages" ? [
|
|
131
170
|
{ label: "Landing pages", value: total.toLocaleString() },
|
|
132
171
|
{ label: "Published", value: published.toLocaleString(), tone: "positive" },
|
|
133
|
-
{ label: "Drafts", value: Math.max(0,
|
|
172
|
+
{ label: "Drafts", value: Math.max(0, total - published).toLocaleString() },
|
|
134
173
|
] : undefined}
|
|
135
174
|
>
|
|
136
175
|
{tab === "pages" ? (
|
|
137
176
|
<>
|
|
138
|
-
{
|
|
177
|
+
<FirstNotice shown={rows.length} limit={rows.length} force={pages.truncated} noun="landing pages" />
|
|
178
|
+
{pages.error ? <ApiError error={pages.error} title="Could not load landing pages" onRetry={pages.refetch} /> : null}
|
|
139
179
|
<SplitView
|
|
140
180
|
detailOpen={Boolean(pageId)}
|
|
141
181
|
onCloseDetail={() => { setPageId(null); setView(null); }}
|
|
@@ -146,7 +186,8 @@ export function PagesScreen({ defaultDomain, renderDetailActions, extraDetailTab
|
|
|
146
186
|
detail={detail}
|
|
147
187
|
/>
|
|
148
188
|
</>
|
|
149
|
-
) : tab === "
|
|
189
|
+
) : tab === "analytics" ? <PageAnalytics />
|
|
190
|
+
: tab === "content" ? <ContentLibrary selectedId={pageId || null} onSelectedChange={(id) => setPageId(id)} />
|
|
150
191
|
: tab === "brand" ? <BrandKitEditor />
|
|
151
192
|
: <SeoReport defaultDomain={defaultDomain} />}
|
|
152
193
|
</ScreenLayout>
|