@graph8/sdk 0.303.0 → 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.
Files changed (664) hide show
  1. package/README.md +81 -0
  2. package/blocks/README.md +32 -0
  3. package/blocks/_foundation/README.md +78 -0
  4. package/blocks/_foundation/block.json +79 -0
  5. package/blocks/_foundation/components/api-error.tsx +62 -0
  6. package/blocks/_foundation/components/confirm-spend-dialog.tsx +142 -0
  7. package/blocks/_foundation/components/empty-state.tsx +24 -0
  8. package/blocks/_foundation/hooks/use-g8-mutation.ts +88 -0
  9. package/blocks/_foundation/hooks/use-g8-query.ts +87 -0
  10. package/blocks/_foundation/hooks/use-job-poll.ts +130 -0
  11. package/blocks/_foundation/hooks/use-paginated.ts +105 -0
  12. package/blocks/_foundation/lib/g8-client.ts +128 -0
  13. package/blocks/_foundation/recipes/verify-proxy-key.ts +18 -0
  14. package/blocks/_foundation/server/proxy.ts +233 -0
  15. package/blocks/_foundation/server/route.ts +30 -0
  16. package/blocks/agents/README.md +133 -0
  17. package/blocks/agents/block.json +223 -0
  18. package/blocks/agents/components/agent-builder.tsx +199 -0
  19. package/blocks/agents/components/agent-knowledge-panel.tsx +144 -0
  20. package/blocks/agents/components/agent-runs.tsx +175 -0
  21. package/blocks/agents/components/agent-skills-panel.tsx +126 -0
  22. package/blocks/agents/components/agent-test-console.tsx +175 -0
  23. package/blocks/agents/lib/agent-types.ts +114 -0
  24. package/blocks/agents/recipes/create-agent-and-attach-skill.ts +50 -0
  25. package/blocks/agents/recipes/run-and-follow-agent-run.ts +46 -0
  26. package/blocks/agents/recipes/test-agent-before-activating.ts +36 -0
  27. package/blocks/analytics/README.md +73 -0
  28. package/blocks/analytics/block.json +178 -0
  29. package/blocks/analytics/components/funnel-chart.tsx +79 -0
  30. package/blocks/analytics/components/kpi-tiles.tsx +74 -0
  31. package/blocks/analytics/components/leaderboard.tsx +86 -0
  32. package/blocks/analytics/components/report-builder.tsx +200 -0
  33. package/blocks/analytics/hooks/use-report.ts +26 -0
  34. package/blocks/analytics/lib/analytics-types.ts +108 -0
  35. package/blocks/analytics/lib/use-debounced.ts +16 -0
  36. package/blocks/analytics/recipes/01-weekly-outbound-report.ts +32 -0
  37. package/blocks/analytics/recipes/02-revenue-snapshot.ts +22 -0
  38. package/blocks/analytics/recipes/03-saved-report.ts +30 -0
  39. package/blocks/appointments/README.md +80 -0
  40. package/blocks/appointments/block.json +218 -0
  41. package/blocks/appointments/components/availability-editor.tsx +192 -0
  42. package/blocks/appointments/components/booking-page.tsx +154 -0
  43. package/blocks/appointments/components/event-type-editor.tsx +214 -0
  44. package/blocks/appointments/components/meeting-detail.tsx +147 -0
  45. package/blocks/appointments/components/meeting-list.tsx +148 -0
  46. package/blocks/appointments/hooks/use-available-slots.ts +29 -0
  47. package/blocks/appointments/hooks/use-schedules.ts +18 -0
  48. package/blocks/appointments/lib/appointments-types.ts +159 -0
  49. package/blocks/appointments/lib/idempotency.ts +30 -0
  50. package/blocks/appointments/recipes/01-create-event-type-and-hours.ts +27 -0
  51. package/blocks/appointments/recipes/02-find-slots-and-book.ts +34 -0
  52. package/blocks/appointments/recipes/03-bookings-digest.ts +22 -0
  53. package/blocks/appointments/recipes/04-calendar-invite.ts +21 -0
  54. package/blocks/block.schema.json +130 -0
  55. package/blocks/block.types.ts +109 -0
  56. package/blocks/campaigns/README.md +118 -0
  57. package/blocks/campaigns/block.json +361 -0
  58. package/blocks/campaigns/components/campaign-assets.tsx +180 -0
  59. package/blocks/campaigns/components/campaign-audience-step.tsx +91 -0
  60. package/blocks/campaigns/components/campaign-launch-step.tsx +147 -0
  61. package/blocks/campaigns/components/campaign-list.tsx +120 -0
  62. package/blocks/campaigns/components/campaign-metrics.tsx +93 -0
  63. package/blocks/campaigns/components/campaign-select.tsx +52 -0
  64. package/blocks/campaigns/components/campaign-sequence-step.tsx +158 -0
  65. package/blocks/campaigns/components/campaign-wizard.tsx +238 -0
  66. package/blocks/campaigns/components/form-field.tsx +29 -0
  67. package/blocks/campaigns/components/icp-editor.tsx +230 -0
  68. package/blocks/campaigns/components/persona-editor.tsx +210 -0
  69. package/blocks/campaigns/hooks/use-company-profile.ts +34 -0
  70. package/blocks/campaigns/lib/campaigns-utils.ts +155 -0
  71. package/blocks/campaigns/recipes/attach-audience-and-launch.ts +33 -0
  72. package/blocks/campaigns/recipes/campaign-report.ts +22 -0
  73. package/blocks/campaigns/recipes/generate-ideas-and-create-campaign.ts +51 -0
  74. package/blocks/campaigns/recipes/set-up-icp-and-personas.ts +37 -0
  75. package/blocks/coach/README.md +93 -0
  76. package/blocks/coach/block.json +125 -0
  77. package/blocks/coach/components/call-scorecard.tsx +126 -0
  78. package/blocks/coach/components/coaching-feed.tsx +140 -0
  79. package/blocks/coach/lib/coach-types.ts +116 -0
  80. package/blocks/coach/recipes/read-a-scorecard.ts +27 -0
  81. package/blocks/coach/recipes/weekly-coaching-digest.ts +53 -0
  82. package/blocks/copilot/README.md +99 -0
  83. package/blocks/copilot/block.json +151 -0
  84. package/blocks/copilot/components/copilot-memory.tsx +87 -0
  85. package/blocks/copilot/components/copilot-panel.tsx +189 -0
  86. package/blocks/copilot/components/copilot-team-chat.tsx +111 -0
  87. package/blocks/copilot/lib/copilot-types.ts +111 -0
  88. package/blocks/copilot/recipes/ask-the-copilot.ts +35 -0
  89. package/blocks/copilot/recipes/post-in-a-work-channel.ts +33 -0
  90. package/blocks/copilot/recipes/remember-and-recall.ts +19 -0
  91. package/blocks/crm/README.md +122 -0
  92. package/blocks/crm/block.json +378 -0
  93. package/blocks/crm/components/activity-timeline.tsx +99 -0
  94. package/blocks/crm/components/company-detail.tsx +118 -0
  95. package/blocks/crm/components/company-table.tsx +173 -0
  96. package/blocks/crm/components/contact-detail.tsx +135 -0
  97. package/blocks/crm/components/contact-table.tsx +217 -0
  98. package/blocks/crm/components/dedupe-review.tsx +164 -0
  99. package/blocks/crm/components/import-wizard.tsx +193 -0
  100. package/blocks/crm/components/notes-panel.tsx +112 -0
  101. package/blocks/crm/components/record-form.tsx +191 -0
  102. package/blocks/crm/components/task-list.tsx +107 -0
  103. package/blocks/crm/hooks/use-custom-fields.ts +18 -0
  104. package/blocks/crm/hooks/use-debounced.ts +13 -0
  105. package/blocks/crm/hooks/use-page-query.ts +52 -0
  106. package/blocks/crm/lib/crm-types.ts +136 -0
  107. package/blocks/crm/recipes/custom-fields-and-values.ts +28 -0
  108. package/blocks/crm/recipes/dedupe-and-merge.ts +30 -0
  109. package/blocks/crm/recipes/notes-tasks-and-activity.ts +23 -0
  110. package/blocks/crm/recipes/upsert-contact-with-company.ts +33 -0
  111. package/blocks/deals/README.md +78 -0
  112. package/blocks/deals/block.json +198 -0
  113. package/blocks/deals/components/deal-detail.tsx +134 -0
  114. package/blocks/deals/components/forecast-view.tsx +112 -0
  115. package/blocks/deals/components/pipeline-board.tsx +142 -0
  116. package/blocks/deals/components/stage-editor.tsx +96 -0
  117. package/blocks/deals/hooks/use-pipeline-deals.ts +67 -0
  118. package/blocks/deals/hooks/use-pipelines.ts +19 -0
  119. package/blocks/deals/lib/deals-types.ts +128 -0
  120. package/blocks/deals/recipes/01-create-deal-and-advance.ts +32 -0
  121. package/blocks/deals/recipes/02-pipeline-report.ts +35 -0
  122. package/blocks/deals/recipes/03-stale-deals-sweep.ts +20 -0
  123. package/blocks/dialer/README.md +114 -0
  124. package/blocks/dialer/block.json +260 -0
  125. package/blocks/dialer/components/call-log.tsx +78 -0
  126. package/blocks/dialer/components/call-queue.tsx +188 -0
  127. package/blocks/dialer/components/dialer-workspace.tsx +47 -0
  128. package/blocks/dialer/components/disposition-panel.tsx +75 -0
  129. package/blocks/dialer/components/live-calls-table.tsx +107 -0
  130. package/blocks/dialer/components/session-start-form.tsx +111 -0
  131. package/blocks/dialer/components/softphone.tsx +122 -0
  132. package/blocks/dialer/components/transcript-viewer.tsx +82 -0
  133. package/blocks/dialer/hooks/use-dialer-numbers.ts +19 -0
  134. package/blocks/dialer/hooks/use-dialer-session.ts +68 -0
  135. package/blocks/dialer/hooks/use-dispositions.ts +29 -0
  136. package/blocks/dialer/lib/dialer-types.ts +158 -0
  137. package/blocks/dialer/lib/softphone-transport.ts +67 -0
  138. package/blocks/dialer/lib/twilio-transport.ts +74 -0
  139. package/blocks/dialer/recipes/01-create-session-and-dial.ts +39 -0
  140. package/blocks/dialer/recipes/02-disposition-calls.ts +26 -0
  141. package/blocks/dialer/recipes/03-review-a-call.ts +23 -0
  142. package/blocks/enrichment/README.md +105 -0
  143. package/blocks/enrichment/block.json +204 -0
  144. package/blocks/enrichment/components/bulk-enrich-panel.tsx +205 -0
  145. package/blocks/enrichment/components/email-verify-badge.tsx +82 -0
  146. package/blocks/enrichment/components/enrich-button.tsx +136 -0
  147. package/blocks/enrichment/components/waterfall-config.tsx +127 -0
  148. package/blocks/enrichment/hooks/use-providers.ts +18 -0
  149. package/blocks/enrichment/lib/enrichment-types.ts +104 -0
  150. package/blocks/enrichment/recipes/bulk-enrich-and-poll.ts +41 -0
  151. package/blocks/enrichment/recipes/lookup-and-verify.ts +25 -0
  152. package/blocks/enrichment/recipes/waterfall-pipeline.ts +26 -0
  153. package/blocks/forms/README.md +112 -0
  154. package/blocks/forms/block.json +200 -0
  155. package/blocks/forms/components/form-builder.tsx +150 -0
  156. package/blocks/forms/components/form-embed.tsx +103 -0
  157. package/blocks/forms/components/form-mapping-editor.tsx +125 -0
  158. package/blocks/forms/components/site-audit-panel.tsx +73 -0
  159. package/blocks/forms/components/submissions-table.tsx +158 -0
  160. package/blocks/forms/lib/form-types.ts +127 -0
  161. package/blocks/forms/recipes/install-snippet-and-form.ts +21 -0
  162. package/blocks/forms/recipes/retry-failed-deliveries.ts +26 -0
  163. package/blocks/forms/recipes/route-form-to-list.ts +44 -0
  164. package/blocks/inbox/README.md +72 -0
  165. package/blocks/inbox/block.json +194 -0
  166. package/blocks/inbox/components/inbox-list.tsx +156 -0
  167. package/blocks/inbox/components/inbox-screen.tsx +49 -0
  168. package/blocks/inbox/components/reply-composer.tsx +183 -0
  169. package/blocks/inbox/components/tag-and-assign.tsx +189 -0
  170. package/blocks/inbox/components/thread-badge.tsx +17 -0
  171. package/blocks/inbox/components/thread-view.tsx +108 -0
  172. package/blocks/inbox/hooks/use-inbox-thread.ts +24 -0
  173. package/blocks/inbox/hooks/use-inbox-threads.ts +40 -0
  174. package/blocks/inbox/lib/idempotency.ts +30 -0
  175. package/blocks/inbox/lib/inbox-types.ts +100 -0
  176. package/blocks/inbox/recipes/01-triage-and-reply.ts +43 -0
  177. package/blocks/inbox/recipes/02-tag-and-assign.ts +31 -0
  178. package/blocks/inbox/recipes/03-hot-contacts-digest.ts +16 -0
  179. package/blocks/integrations/README.md +75 -0
  180. package/blocks/integrations/block.json +172 -0
  181. package/blocks/integrations/components/integration-list.tsx +165 -0
  182. package/blocks/integrations/components/sync-status.tsx +223 -0
  183. package/blocks/integrations/components/webhook-manager.tsx +300 -0
  184. package/blocks/integrations/lib/integrations-utils.ts +62 -0
  185. package/blocks/integrations/recipes/crm-sync-health.ts +19 -0
  186. package/blocks/integrations/recipes/push-contacts-to-crm.ts +20 -0
  187. package/blocks/integrations/recipes/register-webhook.ts +18 -0
  188. package/blocks/integrations/recipes/verify-webhook-route.ts +33 -0
  189. package/blocks/intent/README.md +57 -0
  190. package/blocks/intent/block.json +138 -0
  191. package/blocks/intent/components/account-intent-score.tsx +91 -0
  192. package/blocks/intent/components/intent-feed.tsx +215 -0
  193. package/blocks/intent/components/intent-keyword-manager.tsx +331 -0
  194. package/blocks/intent/hooks/use-account-intent.ts +46 -0
  195. package/blocks/intent/lib/intent-utils.ts +95 -0
  196. package/blocks/intent/recipes/rank-intent-accounts.ts +36 -0
  197. package/blocks/intent/recipes/track-competitor-domain.ts +23 -0
  198. package/blocks/intent/recipes/who-visited-url.ts +18 -0
  199. package/blocks/lifecycle/README.md +79 -0
  200. package/blocks/lifecycle/block.json +198 -0
  201. package/blocks/lifecycle/components/churn-list.tsx +92 -0
  202. package/blocks/lifecycle/components/customer-table.tsx +119 -0
  203. package/blocks/lifecycle/components/lead-queue.tsx +127 -0
  204. package/blocks/lifecycle/components/lifecycle-board.tsx +102 -0
  205. package/blocks/lifecycle/components/stage-move-dialog.tsx +75 -0
  206. package/blocks/lifecycle/hooks/use-debounced.ts +13 -0
  207. package/blocks/lifecycle/hooks/use-lifecycle-stages.ts +27 -0
  208. package/blocks/lifecycle/lib/lifecycle.ts +99 -0
  209. package/blocks/lifecycle/recipes/customer-brief.ts +23 -0
  210. package/blocks/lifecycle/recipes/move-company-stage.ts +17 -0
  211. package/blocks/lifecycle/recipes/read-lifecycle-board.ts +21 -0
  212. package/blocks/lifecycle/recipes/unlock-stage-contacts.ts +25 -0
  213. package/blocks/lists/README.md +96 -0
  214. package/blocks/lists/block.json +166 -0
  215. package/blocks/lists/components/add-to-list-button.tsx +111 -0
  216. package/blocks/lists/components/list-picker.tsx +94 -0
  217. package/blocks/lists/components/list-table.tsx +160 -0
  218. package/blocks/lists/components/suppression-manager.tsx +153 -0
  219. package/blocks/lists/recipes/create-list-and-add-contacts.ts +35 -0
  220. package/blocks/lists/recipes/refresh-and-check-lists.ts +21 -0
  221. package/blocks/lists/recipes/suppress-and-reinstate.ts +30 -0
  222. package/blocks/mailboxes/README.md +71 -0
  223. package/blocks/mailboxes/block.json +187 -0
  224. package/blocks/mailboxes/components/connect-mailbox.tsx +284 -0
  225. package/blocks/mailboxes/components/domain-purchase.tsx +271 -0
  226. package/blocks/mailboxes/components/mailbox-list.tsx +185 -0
  227. package/blocks/mailboxes/components/warmup-health.tsx +232 -0
  228. package/blocks/mailboxes/lib/mailboxes-utils.ts +69 -0
  229. package/blocks/mailboxes/recipes/buy-domain-and-mailboxes.ts +64 -0
  230. package/blocks/mailboxes/recipes/connect-imap-mailbox.ts +38 -0
  231. package/blocks/mailboxes/recipes/is-it-safe-to-send.ts +17 -0
  232. package/blocks/mailboxes/recipes/warmup-report.ts +31 -0
  233. package/blocks/newsletter/README.md +122 -0
  234. package/blocks/newsletter/block.json +224 -0
  235. package/blocks/newsletter/components/issue-form.tsx +124 -0
  236. package/blocks/newsletter/components/issue-scheduler.tsx +152 -0
  237. package/blocks/newsletter/components/newsletter-editor.tsx +131 -0
  238. package/blocks/newsletter/components/newsletter-stats.tsx +93 -0
  239. package/blocks/newsletter/components/subscriber-table.tsx +173 -0
  240. package/blocks/newsletter/lib/newsletter-types.ts +97 -0
  241. package/blocks/newsletter/recipes/create-newsletter-and-draft-issue.ts +27 -0
  242. package/blocks/newsletter/recipes/import-audience-and-send.ts +43 -0
  243. package/blocks/newsletter/recipes/issue-performance-report.ts +26 -0
  244. package/blocks/nurture/README.md +110 -0
  245. package/blocks/nurture/block.json +191 -0
  246. package/blocks/nurture/components/nurture-builder.tsx +114 -0
  247. package/blocks/nurture/components/nurture-enroll.tsx +107 -0
  248. package/blocks/nurture/components/nurture-form.tsx +120 -0
  249. package/blocks/nurture/components/nurture-stats.tsx +81 -0
  250. package/blocks/nurture/lib/nurture-types.ts +109 -0
  251. package/blocks/nurture/recipes/create-a-nurture.ts +34 -0
  252. package/blocks/nurture/recipes/enroll-contacts-and-launch.ts +24 -0
  253. package/blocks/nurture/recipes/nurture-health-check.ts +28 -0
  254. package/blocks/objects/README.md +120 -0
  255. package/blocks/objects/block.json +243 -0
  256. package/blocks/objects/components/attribute-field-input.tsx +82 -0
  257. package/blocks/objects/components/attribute-form-dialog.tsx +159 -0
  258. package/blocks/objects/components/object-not-found.tsx +34 -0
  259. package/blocks/objects/components/object-record-detail.tsx +143 -0
  260. package/blocks/objects/components/object-schema-editor.tsx +197 -0
  261. package/blocks/objects/components/object-table.tsx +130 -0
  262. package/blocks/objects/components/record-form-dialog.tsx +112 -0
  263. package/blocks/objects/components/record-roles-panel.tsx +106 -0
  264. package/blocks/objects/hooks/use-object-schema.ts +36 -0
  265. package/blocks/objects/lib/object-types.ts +214 -0
  266. package/blocks/objects/recipes/define-object-and-add-records.ts +24 -0
  267. package/blocks/objects/recipes/update-audit-and-archive-records.ts +39 -0
  268. package/blocks/objects/recipes/upsert-records-from-your-system.ts +30 -0
  269. package/blocks/pages/README.md +104 -0
  270. package/blocks/pages/block.json +223 -0
  271. package/blocks/pages/components/brand-kit-editor.tsx +152 -0
  272. package/blocks/pages/components/content-library.tsx +163 -0
  273. package/blocks/pages/components/page-editor.tsx +253 -0
  274. package/blocks/pages/components/page-list.tsx +159 -0
  275. package/blocks/pages/components/seo-report.tsx +159 -0
  276. package/blocks/pages/lib/studio.ts +77 -0
  277. package/blocks/pages/recipes/audit-site-seo.ts +28 -0
  278. package/blocks/pages/recipes/create-and-publish-page.ts +26 -0
  279. package/blocks/pages/recipes/edit-page-section.ts +25 -0
  280. package/blocks/pages/recipes/refresh-brand-kit.ts +22 -0
  281. package/blocks/quotes/README.md +79 -0
  282. package/blocks/quotes/block.json +183 -0
  283. package/blocks/quotes/components/product-picker.tsx +80 -0
  284. package/blocks/quotes/components/quote-builder.tsx +195 -0
  285. package/blocks/quotes/components/quote-list.tsx +79 -0
  286. package/blocks/quotes/components/quote-viewer.tsx +135 -0
  287. package/blocks/quotes/components/signature-status.tsx +77 -0
  288. package/blocks/quotes/hooks/use-debounced.ts +13 -0
  289. package/blocks/quotes/lib/quotes.ts +142 -0
  290. package/blocks/quotes/recipes/create-and-send-quote.ts +44 -0
  291. package/blocks/quotes/recipes/revise-sent-quote.ts +18 -0
  292. package/blocks/quotes/recipes/track-quote-signature.ts +22 -0
  293. package/blocks/radar/README.md +106 -0
  294. package/blocks/radar/block.json +189 -0
  295. package/blocks/radar/components/competitor-battle-card.tsx +53 -0
  296. package/blocks/radar/components/competitor-tracker.tsx +158 -0
  297. package/blocks/radar/components/radar-overview.tsx +67 -0
  298. package/blocks/radar/components/radar-report.tsx +86 -0
  299. package/blocks/radar/hooks/use-competitors.ts +23 -0
  300. package/blocks/radar/lib/radar-types.ts +124 -0
  301. package/blocks/radar/recipes/battle-card.ts +28 -0
  302. package/blocks/radar/recipes/deep-dive-report.ts +32 -0
  303. package/blocks/radar/recipes/marketing-snapshot.ts +18 -0
  304. package/blocks/radar/recipes/track-and-scan.ts +39 -0
  305. package/blocks/search/README.md +129 -0
  306. package/blocks/search/block.json +185 -0
  307. package/blocks/search/components/filter-builder.tsx +149 -0
  308. package/blocks/search/components/prospect-results.tsx +175 -0
  309. package/blocks/search/components/save-to-list-dialog.tsx +135 -0
  310. package/blocks/search/components/saved-searches.tsx +95 -0
  311. package/blocks/search/hooks/use-filter-options.ts +41 -0
  312. package/blocks/search/lib/search-types.ts +149 -0
  313. package/blocks/search/recipes/explore-filter-values.ts +27 -0
  314. package/blocks/search/recipes/saved-searches.ts +34 -0
  315. package/blocks/search/recipes/search-and-save-to-list.ts +37 -0
  316. package/blocks/sequencer/README.md +87 -0
  317. package/blocks/sequencer/block.json +214 -0
  318. package/blocks/sequencer/components/enroll-dialog.tsx +93 -0
  319. package/blocks/sequencer/components/sequence-builder.tsx +306 -0
  320. package/blocks/sequencer/components/sequence-list.tsx +200 -0
  321. package/blocks/sequencer/components/sequence-stats.tsx +256 -0
  322. package/blocks/sequencer/components/step-editor.tsx +213 -0
  323. package/blocks/sequencer/lib/sequencer-utils.ts +168 -0
  324. package/blocks/sequencer/recipes/create-multichannel-sequence.ts +51 -0
  325. package/blocks/sequencer/recipes/launch-and-enroll.ts +44 -0
  326. package/blocks/sequencer/recipes/pause-when-unsafe.ts +30 -0
  327. package/blocks/sequencer/recipes/sequence-report.ts +22 -0
  328. package/blocks/social-ads/README.md +128 -0
  329. package/blocks/social-ads/block.json +256 -0
  330. package/blocks/social-ads/components/ad-campaign-list.tsx +136 -0
  331. package/blocks/social-ads/components/audience-sync.tsx +150 -0
  332. package/blocks/social-ads/components/creative-gallery.tsx +121 -0
  333. package/blocks/social-ads/components/post-composer.tsx +122 -0
  334. package/blocks/social-ads/components/social-post-list.tsx +116 -0
  335. package/blocks/social-ads/hooks/use-social-accounts.ts +16 -0
  336. package/blocks/social-ads/lib/social-ads-types.ts +167 -0
  337. package/blocks/social-ads/recipes/creative-to-gallery.ts +41 -0
  338. package/blocks/social-ads/recipes/schedule-post.ts +51 -0
  339. package/blocks/social-ads/recipes/sync-audience-and-launch.ts +41 -0
  340. package/blocks/visitors/README.md +56 -0
  341. package/blocks/visitors/block.json +139 -0
  342. package/blocks/visitors/components/snippet-install.tsx +169 -0
  343. package/blocks/visitors/components/traffic-chart.tsx +156 -0
  344. package/blocks/visitors/components/visitor-company-card.tsx +92 -0
  345. package/blocks/visitors/components/visitor-feed.tsx +157 -0
  346. package/blocks/visitors/lib/visitors-utils.ts +78 -0
  347. package/blocks/visitors/recipes/check-snippet-installed.ts +24 -0
  348. package/blocks/visitors/recipes/recent-company-visits.ts +27 -0
  349. package/blocks/visitors/recipes/traffic-summary.ts +24 -0
  350. package/blocks/webchat/README.md +87 -0
  351. package/blocks/webchat/block.json +134 -0
  352. package/blocks/webchat/components/chat-widget-config.tsx +160 -0
  353. package/blocks/webchat/components/conversation-list.tsx +74 -0
  354. package/blocks/webchat/lib/webchat-types.ts +83 -0
  355. package/blocks/webchat/recipes/create-bot-and-style-widget.ts +24 -0
  356. package/blocks/webchat/recipes/list-chat-visitors.ts +24 -0
  357. package/blocks/workbench/README.md +135 -0
  358. package/blocks/workbench/block.json +259 -0
  359. package/blocks/workbench/components/column-recipe-editor.tsx +183 -0
  360. package/blocks/workbench/components/run-progress.tsx +132 -0
  361. package/blocks/workbench/components/workbench-add-rows.tsx +100 -0
  362. package/blocks/workbench/components/workbench-grid.tsx +159 -0
  363. package/blocks/workbench/components/workbench-row-actions.tsx +102 -0
  364. package/blocks/workbench/components/workbench-table-list.tsx +105 -0
  365. package/blocks/workbench/hooks/use-workbench-table.ts +30 -0
  366. package/blocks/workbench/lib/waterfall-presets.ts +119 -0
  367. package/blocks/workbench/lib/workbench-types.ts +191 -0
  368. package/blocks/workbench/recipes/build-enriched-table.ts +65 -0
  369. package/blocks/workbench/recipes/import-from-search.ts +42 -0
  370. package/blocks/workbench/recipes/scrape-into-table.ts +36 -0
  371. package/blocks/workbench/recipes/waterfall-column.ts +55 -0
  372. package/blocks/workflows/README.md +100 -0
  373. package/blocks/workflows/block.json +188 -0
  374. package/blocks/workflows/components/run-history.tsx +136 -0
  375. package/blocks/workflows/components/trigger-config.tsx +134 -0
  376. package/blocks/workflows/components/workflow-editor.tsx +142 -0
  377. package/blocks/workflows/components/workflow-list.tsx +153 -0
  378. package/blocks/workflows/hooks/use-workflow.ts +51 -0
  379. package/blocks/workflows/lib/graph.ts +187 -0
  380. package/blocks/workflows/recipes/create-workflow.ts +35 -0
  381. package/blocks/workflows/recipes/find-failed-runs.ts +15 -0
  382. package/blocks/workflows/recipes/run-workflow-and-wait.ts +32 -0
  383. package/blocks/workflows/recipes/set-trigger.ts +22 -0
  384. package/dist/cli.js +5330 -29
  385. package/dist/index.browser.js +7740 -0
  386. package/dist/index.browser.js.map +1 -0
  387. package/dist/index.browser.mjs +7717 -0
  388. package/dist/index.browser.mjs.map +1 -0
  389. package/dist/index.d.mts +2220 -23
  390. package/dist/index.d.ts +2220 -23
  391. package/dist/index.js +64 -3
  392. package/dist/index.js.map +1 -1
  393. package/dist/index.mjs +64 -3
  394. package/dist/index.mjs.map +1 -1
  395. package/dist/react.browser.js +7598 -0
  396. package/dist/react.browser.js.map +1 -0
  397. package/dist/react.browser.mjs +7568 -0
  398. package/dist/react.browser.mjs.map +1 -0
  399. package/dist/react.d.mts +2082 -20
  400. package/dist/react.d.ts +2082 -20
  401. package/dist/react.js +63 -2
  402. package/dist/react.js.map +1 -1
  403. package/dist/react.mjs +63 -2
  404. package/dist/react.mjs.map +1 -1
  405. package/dist/types/agentSetup.d.ts +6 -6
  406. package/dist/types/blocks/add.d.ts +53 -0
  407. package/dist/types/blocks/create.d.ts +46 -0
  408. package/dist/types/blocks/doctor.d.ts +26 -0
  409. package/dist/types/blocks/registry.d.ts +134 -0
  410. package/dist/types/cli.d.ts +16 -0
  411. package/dist/types/core.d.ts +1 -1
  412. package/dist/types/crypto-browser.d.ts +2 -0
  413. package/dist/types/crypto-node.d.ts +6 -0
  414. package/dist/types/react.d.ts +1 -1
  415. package/dist/types/version.d.ts +2 -2
  416. package/dist/types/webhooks.d.ts +2 -2
  417. package/dist/ui/index.d.mts +54 -0
  418. package/dist/ui/index.d.ts +54 -0
  419. package/dist/ui/index.js +105 -0
  420. package/dist/ui/index.js.map +1 -0
  421. package/dist/ui/index.mjs +44 -0
  422. package/dist/ui/index.mjs.map +1 -0
  423. package/dist/ui/lib/compose-refs.d.mts +19 -0
  424. package/dist/ui/lib/compose-refs.d.ts +19 -0
  425. package/dist/ui/lib/compose-refs.js +76 -0
  426. package/dist/ui/lib/compose-refs.js.map +1 -0
  427. package/dist/ui/lib/compose-refs.mjs +41 -0
  428. package/dist/ui/lib/compose-refs.mjs.map +1 -0
  429. package/dist/ui/lib/use-mobile.d.mts +4 -0
  430. package/dist/ui/lib/use-mobile.d.ts +4 -0
  431. package/dist/ui/lib/use-mobile.js +57 -0
  432. package/dist/ui/lib/use-mobile.js.map +1 -0
  433. package/dist/ui/lib/use-mobile.mjs +22 -0
  434. package/dist/ui/lib/use-mobile.mjs.map +1 -0
  435. package/dist/ui/lib/utils.d.mts +10 -0
  436. package/dist/ui/lib/utils.d.ts +10 -0
  437. package/dist/ui/lib/utils.js +33 -0
  438. package/dist/ui/lib/utils.js.map +1 -0
  439. package/dist/ui/lib/utils.mjs +9 -0
  440. package/dist/ui/lib/utils.mjs.map +1 -0
  441. package/dist/ui/primitives/alert-dialog.d.mts +22 -0
  442. package/dist/ui/primitives/alert-dialog.d.ts +22 -0
  443. package/dist/ui/primitives/alert-dialog.js +163 -0
  444. package/dist/ui/primitives/alert-dialog.js.map +1 -0
  445. package/dist/ui/primitives/alert-dialog.mjs +119 -0
  446. package/dist/ui/primitives/alert-dialog.mjs.map +1 -0
  447. package/dist/ui/primitives/avatar.d.mts +8 -0
  448. package/dist/ui/primitives/avatar.d.ts +8 -0
  449. package/dist/ui/primitives/avatar.js +91 -0
  450. package/dist/ui/primitives/avatar.js.map +1 -0
  451. package/dist/ui/primitives/avatar.mjs +55 -0
  452. package/dist/ui/primitives/avatar.mjs.map +1 -0
  453. package/dist/ui/primitives/breadcrumb.d.mts +13 -0
  454. package/dist/ui/primitives/breadcrumb.d.ts +13 -0
  455. package/dist/ui/primitives/breadcrumb.js +127 -0
  456. package/dist/ui/primitives/breadcrumb.js.map +1 -0
  457. package/dist/ui/primitives/breadcrumb.mjs +97 -0
  458. package/dist/ui/primitives/breadcrumb.mjs.map +1 -0
  459. package/dist/ui/primitives/button.d.mts +15 -0
  460. package/dist/ui/primitives/button.d.ts +15 -0
  461. package/dist/ui/primitives/button.js +91 -0
  462. package/dist/ui/primitives/button.js.map +1 -0
  463. package/dist/ui/primitives/button.mjs +56 -0
  464. package/dist/ui/primitives/button.mjs.map +1 -0
  465. package/dist/ui/primitives/card.d.mts +10 -0
  466. package/dist/ui/primitives/card.d.ts +10 -0
  467. package/dist/ui/primitives/card.js +101 -0
  468. package/dist/ui/primitives/card.js.map +1 -0
  469. package/dist/ui/primitives/card.mjs +62 -0
  470. package/dist/ui/primitives/card.mjs.map +1 -0
  471. package/dist/ui/primitives/carousel.d.mts +45 -0
  472. package/dist/ui/primitives/carousel.d.ts +45 -0
  473. package/dist/ui/primitives/carousel.js +300 -0
  474. package/dist/ui/primitives/carousel.js.map +1 -0
  475. package/dist/ui/primitives/carousel.mjs +261 -0
  476. package/dist/ui/primitives/carousel.mjs.map +1 -0
  477. package/dist/ui/primitives/chart.d.mts +68 -0
  478. package/dist/ui/primitives/chart.d.ts +68 -0
  479. package/dist/ui/primitives/chart.js +280 -0
  480. package/dist/ui/primitives/chart.js.map +1 -0
  481. package/dist/ui/primitives/chart.mjs +241 -0
  482. package/dist/ui/primitives/chart.mjs.map +1 -0
  483. package/dist/ui/primitives/collapsible.d.mts +8 -0
  484. package/dist/ui/primitives/collapsible.d.ts +8 -0
  485. package/dist/ui/primitives/collapsible.js +72 -0
  486. package/dist/ui/primitives/collapsible.js.map +1 -0
  487. package/dist/ui/primitives/collapsible.mjs +36 -0
  488. package/dist/ui/primitives/collapsible.mjs.map +1 -0
  489. package/dist/ui/primitives/context-menu.d.mts +29 -0
  490. package/dist/ui/primitives/context-menu.d.ts +29 -0
  491. package/dist/ui/primitives/context-menu.js +203 -0
  492. package/dist/ui/primitives/context-menu.js.map +1 -0
  493. package/dist/ui/primitives/context-menu.mjs +155 -0
  494. package/dist/ui/primitives/context-menu.mjs.map +1 -0
  495. package/dist/ui/primitives/dialog.d.mts +26 -0
  496. package/dist/ui/primitives/dialog.d.ts +26 -0
  497. package/dist/ui/primitives/dialog.js +192 -0
  498. package/dist/ui/primitives/dialog.js.map +1 -0
  499. package/dist/ui/primitives/dialog.mjs +148 -0
  500. package/dist/ui/primitives/dialog.mjs.map +1 -0
  501. package/dist/ui/primitives/drawer.d.mts +26 -0
  502. package/dist/ui/primitives/drawer.d.ts +26 -0
  503. package/dist/ui/primitives/drawer.js +146 -0
  504. package/dist/ui/primitives/drawer.js.map +1 -0
  505. package/dist/ui/primitives/drawer.mjs +103 -0
  506. package/dist/ui/primitives/drawer.mjs.map +1 -0
  507. package/dist/ui/primitives/input.d.mts +5 -0
  508. package/dist/ui/primitives/input.d.ts +5 -0
  509. package/dist/ui/primitives/input.js +46 -0
  510. package/dist/ui/primitives/input.js.map +1 -0
  511. package/dist/ui/primitives/input.mjs +22 -0
  512. package/dist/ui/primitives/input.mjs.map +1 -0
  513. package/dist/ui/primitives/item.d.mts +27 -0
  514. package/dist/ui/primitives/item.d.ts +27 -0
  515. package/dist/ui/primitives/item.js +221 -0
  516. package/dist/ui/primitives/item.js.map +1 -0
  517. package/dist/ui/primitives/item.mjs +188 -0
  518. package/dist/ui/primitives/item.mjs.map +1 -0
  519. package/dist/ui/primitives/kbd.d.mts +15 -0
  520. package/dist/ui/primitives/kbd.d.ts +15 -0
  521. package/dist/ui/primitives/kbd.js +45 -0
  522. package/dist/ui/primitives/kbd.js.map +1 -0
  523. package/dist/ui/primitives/kbd.mjs +21 -0
  524. package/dist/ui/primitives/kbd.mjs.map +1 -0
  525. package/dist/ui/primitives/label.d.mts +6 -0
  526. package/dist/ui/primitives/label.d.ts +6 -0
  527. package/dist/ui/primitives/label.js +55 -0
  528. package/dist/ui/primitives/label.js.map +1 -0
  529. package/dist/ui/primitives/label.mjs +21 -0
  530. package/dist/ui/primitives/label.mjs.map +1 -0
  531. package/dist/ui/primitives/resizable.d.mts +10 -0
  532. package/dist/ui/primitives/resizable.d.ts +10 -0
  533. package/dist/ui/primitives/resizable.js +68 -0
  534. package/dist/ui/primitives/resizable.js.map +1 -0
  535. package/dist/ui/primitives/resizable.mjs +42 -0
  536. package/dist/ui/primitives/resizable.mjs.map +1 -0
  537. package/dist/ui/primitives/save-status.d.mts +31 -0
  538. package/dist/ui/primitives/save-status.d.ts +31 -0
  539. package/dist/ui/primitives/save-status.js +150 -0
  540. package/dist/ui/primitives/save-status.js.map +1 -0
  541. package/dist/ui/primitives/save-status.mjs +126 -0
  542. package/dist/ui/primitives/save-status.mjs.map +1 -0
  543. package/dist/ui/primitives/separator.d.mts +6 -0
  544. package/dist/ui/primitives/separator.d.ts +6 -0
  545. package/dist/ui/primitives/separator.js +62 -0
  546. package/dist/ui/primitives/separator.js.map +1 -0
  547. package/dist/ui/primitives/separator.mjs +28 -0
  548. package/dist/ui/primitives/separator.mjs.map +1 -0
  549. package/dist/ui/primitives/sheet.d.mts +16 -0
  550. package/dist/ui/primitives/sheet.d.ts +16 -0
  551. package/dist/ui/primitives/sheet.js +170 -0
  552. package/dist/ui/primitives/sheet.js.map +1 -0
  553. package/dist/ui/primitives/sheet.mjs +129 -0
  554. package/dist/ui/primitives/sheet.mjs.map +1 -0
  555. package/dist/ui/primitives/sidebar.d.mts +74 -0
  556. package/dist/ui/primitives/sidebar.d.ts +74 -0
  557. package/dist/ui/primitives/sidebar.js +697 -0
  558. package/dist/ui/primitives/sidebar.js.map +1 -0
  559. package/dist/ui/primitives/sidebar.mjs +651 -0
  560. package/dist/ui/primitives/sidebar.mjs.map +1 -0
  561. package/dist/ui/primitives/skeleton.d.mts +5 -0
  562. package/dist/ui/primitives/skeleton.d.ts +5 -0
  563. package/dist/ui/primitives/skeleton.js +40 -0
  564. package/dist/ui/primitives/skeleton.js.map +1 -0
  565. package/dist/ui/primitives/skeleton.mjs +16 -0
  566. package/dist/ui/primitives/skeleton.mjs.map +1 -0
  567. package/dist/ui/primitives/slider.d.mts +20 -0
  568. package/dist/ui/primitives/slider.d.ts +20 -0
  569. package/dist/ui/primitives/slider.js +112 -0
  570. package/dist/ui/primitives/slider.js.map +1 -0
  571. package/dist/ui/primitives/slider.mjs +78 -0
  572. package/dist/ui/primitives/slider.mjs.map +1 -0
  573. package/dist/ui/primitives/sortable.d.mts +50 -0
  574. package/dist/ui/primitives/sortable.d.ts +50 -0
  575. package/dist/ui/primitives/sortable.js +456 -0
  576. package/dist/ui/primitives/sortable.js.map +1 -0
  577. package/dist/ui/primitives/sortable.mjs +435 -0
  578. package/dist/ui/primitives/sortable.mjs.map +1 -0
  579. package/dist/ui/primitives/switch.d.mts +6 -0
  580. package/dist/ui/primitives/switch.d.ts +6 -0
  581. package/dist/ui/primitives/switch.js +67 -0
  582. package/dist/ui/primitives/switch.js.map +1 -0
  583. package/dist/ui/primitives/switch.mjs +33 -0
  584. package/dist/ui/primitives/switch.mjs.map +1 -0
  585. package/dist/ui/primitives/table.d.mts +12 -0
  586. package/dist/ui/primitives/table.d.ts +12 -0
  587. package/dist/ui/primitives/table.js +130 -0
  588. package/dist/ui/primitives/table.js.map +1 -0
  589. package/dist/ui/primitives/table.mjs +89 -0
  590. package/dist/ui/primitives/table.mjs.map +1 -0
  591. package/dist/ui/primitives/tabs.d.mts +9 -0
  592. package/dist/ui/primitives/tabs.d.ts +9 -0
  593. package/dist/ui/primitives/tabs.js +86 -0
  594. package/dist/ui/primitives/tabs.js.map +1 -0
  595. package/dist/ui/primitives/tabs.mjs +49 -0
  596. package/dist/ui/primitives/tabs.mjs.map +1 -0
  597. package/dist/ui/primitives/textarea.d.mts +5 -0
  598. package/dist/ui/primitives/textarea.d.ts +5 -0
  599. package/dist/ui/primitives/textarea.js +43 -0
  600. package/dist/ui/primitives/textarea.js.map +1 -0
  601. package/dist/ui/primitives/textarea.mjs +19 -0
  602. package/dist/ui/primitives/textarea.mjs.map +1 -0
  603. package/dist/ui/primitives/tooltip.d.mts +9 -0
  604. package/dist/ui/primitives/tooltip.d.ts +9 -0
  605. package/dist/ui/primitives/tooltip.js +94 -0
  606. package/dist/ui/primitives/tooltip.js.map +1 -0
  607. package/dist/ui/primitives/tooltip.mjs +57 -0
  608. package/dist/ui/primitives/tooltip.mjs.map +1 -0
  609. package/docs/agents.md +72 -29
  610. package/docs/api/README.md +7 -3
  611. package/docs/api/appObjects.d.ts +3 -1
  612. package/docs/api/appObjects.md +2 -1
  613. package/docs/api/by-path.md +51 -0
  614. package/docs/api/dataPipelines.d.ts +149 -0
  615. package/docs/api/dataPipelines.md +232 -0
  616. package/docs/api/forms.d.ts +16 -0
  617. package/docs/api/forms.md +8 -0
  618. package/docs/api/objects.d.ts +705 -5
  619. package/docs/api/objects.md +1099 -14
  620. package/docs/api/ops.d.ts +1 -1
  621. package/docs/api/ops.md +1 -1
  622. package/docs/api/processLists.d.ts +247 -0
  623. package/docs/api/processLists.md +409 -0
  624. package/docs/api/quotes.d.ts +3 -1
  625. package/docs/api/quotes.md +7 -2
  626. package/docs/api/radar.d.ts +4 -0
  627. package/docs/api/radar.md +1 -0
  628. package/docs/api/reports.d.ts +476 -0
  629. package/docs/api/reports.md +539 -0
  630. package/docs/api/scheduleBuy.d.ts +12 -8
  631. package/docs/api/scheduleBuy.md +13 -10
  632. package/docs/api/seo.d.ts +406 -3
  633. package/docs/api/seo.md +505 -1
  634. package/docs/api/workspaceBackup.d.ts +324 -0
  635. package/docs/api/workspaceBackup.md +427 -0
  636. package/package.json +174 -5
  637. package/stacks/6sense.json +46 -0
  638. package/stacks/README.md +20 -0
  639. package/stacks/apollo.json +128 -0
  640. package/stacks/clay.json +59 -0
  641. package/stacks/gong.json +72 -0
  642. package/stacks/hubspot.json +162 -0
  643. package/stacks/intercom.json +53 -0
  644. package/stacks/mailchimp.json +71 -0
  645. package/stacks/outreach.json +86 -0
  646. package/stacks/salesforce.json +94 -0
  647. package/stacks/stack.schema.json +32 -0
  648. package/stacks/starter.json +22 -0
  649. package/styles/styles.css +2 -0
  650. package/styles/theme.css +181 -0
  651. package/styles/tokens.css +284 -0
  652. package/templates/next-app/README.md.tmpl +35 -0
  653. package/templates/next-app/_pages/redirect-page.tsx.tmpl +5 -0
  654. package/templates/next-app/_pages/route-page.tsx.tmpl +9 -0
  655. package/templates/next-app/app/globals.css.tmpl +8 -0
  656. package/templates/next-app/app/layout.tsx.tmpl +19 -0
  657. package/templates/next-app/components/app-shell.tsx.tmpl +34 -0
  658. package/templates/next-app/dot-env.local.example.tmpl +6 -0
  659. package/templates/next-app/dot-gitignore.tmpl +7 -0
  660. package/templates/next-app/lib/nav.ts.tmpl +2 -0
  661. package/templates/next-app/next.config.mjs.tmpl +4 -0
  662. package/templates/next-app/package.json.tmpl +25 -0
  663. package/templates/next-app/postcss.config.mjs.tmpl +4 -0
  664. package/templates/next-app/tsconfig.json.tmpl +21 -0
@@ -0,0 +1,131 @@
1
+ "use client";
2
+
3
+ /**
4
+ * NewsletterEditor: pick or create a newsletter, see its issues, write one, and take the newsletter live.
5
+ *
6
+ * SDK operations, in order:
7
+ * 1. listNewsletters (GET /newsletters) read
8
+ * 2. createNewsletter (POST /newsletters) external -> ConfirmSpendDialog (creates the container; sends nothing)
9
+ * 3. listIssues (GET /newsletters/{id}/issues) read (each row carries inline KPIs once sent)
10
+ * 4. IssueForm: getIssue, createIssue/updateIssue, generateNewsletterIssue, sendTestEmail, deleteIssue
11
+ * 5. activateNewsletter (POST /newsletters/{id}/activate) external -> ConfirmSpendDialog (LAUNCH-CLASS: the scheduler may now mail due issues)
12
+ * 6. pauseNewsletter (POST /newsletters/{id}/pause) write (stops further sends)
13
+ * Sending an issue is NOT here: use IssueScheduler, which confirms with the subscriber count.
14
+ */
15
+
16
+ import { useState } from "react";
17
+ import { Button, Card, CardContent, CardHeader, CardTitle, Input, Label, Skeleton } from "@graph8/sdk/ui";
18
+ import { ApiError } from "@/g8/components/api-error";
19
+ import { ConfirmSpendDialog } from "@/g8/components/confirm-spend-dialog";
20
+ import { EmptyState } from "@/g8/components/empty-state";
21
+ import { g8Get, g8Post } from "@/g8/lib/g8-client";
22
+ import { useG8Mutation } from "@/g8/hooks/use-g8-mutation";
23
+ import { useG8Query } from "@/g8/hooks/use-g8-query";
24
+ import { issueTitle } from "../lib/newsletter-types";
25
+ import type { Issue, Newsletter } from "../lib/newsletter-types";
26
+ import { IssueForm } from "./issue-form";
27
+
28
+ export interface NewsletterEditorProps {
29
+ initialNewsletterId?: string;
30
+ /** Called when a sent-ready draft is chosen, so the page can show IssueScheduler. */
31
+ onSelectIssue?: (newsletterId: string, issue: Issue) => void;
32
+ }
33
+
34
+ type Pending = { kind: "create" } | { kind: "activate" } | null;
35
+
36
+ export function NewsletterEditor({ initialNewsletterId, onSelectIssue }: NewsletterEditorProps) {
37
+ const [newsletterId, setNewsletterId] = useState<string | undefined>(initialNewsletterId);
38
+ const [issueId, setIssueId] = useState<number | "new" | undefined>(undefined);
39
+ const [name, setName] = useState("");
40
+ const [goal, setGoal] = useState("");
41
+ const [pending, setPending] = useState<Pending>(null);
42
+ const newsletters = useG8Query((signal) => g8Get<Newsletter[]>("/newsletters", {}, { signal }).then((r) => r.data ?? []), []);
43
+ const issues = useG8Query((signal) => g8Get<Issue[]>(`/newsletters/${encodeURIComponent(newsletterId ?? "")}/issues`, {}, { signal }).then((r) => r.data ?? []), [newsletterId], { enabled: Boolean(newsletterId) });
44
+ const current = (newsletters.data ?? []).find((n) => n.id === newsletterId);
45
+
46
+ const create = useG8Mutation(async () => (await g8Post<Newsletter>("/newsletters", { name: name.trim(), goal: goal.trim() || undefined })).data, {
47
+ onSuccess: (created) => { setPending(null); setName(""); setGoal(""); setNewsletterId(created.id); void newsletters.refetch(); },
48
+ });
49
+ const activate = useG8Mutation(() => g8Post(`/newsletters/${encodeURIComponent(newsletterId ?? "")}/activate`), { onSuccess: () => { setPending(null); void newsletters.refetch(); } });
50
+ const pause = useG8Mutation(() => g8Post(`/newsletters/${encodeURIComponent(newsletterId ?? "")}/pause`), { onSuccess: () => void newsletters.refetch() });
51
+
52
+ if (newsletters.error) return <ApiError error={newsletters.error} title="Could not load newsletters" onRetry={() => void newsletters.refetch()} />;
53
+ if (newsletters.loading && !newsletters.data) return <div aria-busy="true" className="grid gap-2"><Skeleton className="h-9 w-64" /><Skeleton className="h-40 w-full" /></div>;
54
+ const list = newsletters.data ?? [];
55
+ const live = current?.status === "active";
56
+ return (
57
+ <div className="grid gap-4 md:grid-cols-[18rem_1fr]">
58
+ <Card>
59
+ <CardHeader><CardTitle>Newsletters</CardTitle></CardHeader>
60
+ <CardContent className="grid gap-3">
61
+ {list.length === 0 ? <p className="text-muted-foreground text-sm">No newsletters yet.</p> : (
62
+ <ul className="grid gap-1">
63
+ {list.map((n) => (
64
+ <li key={n.id}>
65
+ <Button variant={n.id === newsletterId ? "secondary" : "ghost"} size="sm" className="w-full justify-between" onClick={() => { setNewsletterId(n.id); setIssueId(undefined); }}>
66
+ <span>{n.name}</span><span className="text-muted-foreground text-xs">{n.status}</span>
67
+ </Button>
68
+ </li>
69
+ ))}
70
+ </ul>
71
+ )}
72
+ <form className="grid gap-2" onSubmit={(e) => { e.preventDefault(); if (name.trim()) setPending({ kind: "create" }); }}>
73
+ <Label htmlFor="g8-nl-name">New newsletter</Label>
74
+ <Input id="g8-nl-name" placeholder="Name" value={name} onChange={(e) => setName(e.target.value)} />
75
+ <Input aria-label="Goal" placeholder="Goal (optional)" value={goal} onChange={(e) => setGoal(e.target.value)} />
76
+ <Button type="submit" size="sm" disabled={!name.trim()}>Create newsletter</Button>
77
+ </form>
78
+ </CardContent>
79
+ </Card>
80
+ <div className="grid content-start gap-4">
81
+ {!current ? <EmptyState title="Pick a newsletter" description="Choose a newsletter to see its issues, or create one." /> : (
82
+ <>
83
+ <div className="flex items-center justify-between">
84
+ <h2 className="text-lg font-semibold">{current.name} <span className="text-muted-foreground text-sm font-normal">{current.status}</span></h2>
85
+ {live ? <Button variant="outline" size="sm" loading={pause.loading} onClick={() => void pause.mutate(undefined)}>Pause newsletter</Button> : <Button size="sm" onClick={() => setPending({ kind: "activate" })}>Activate newsletter</Button>}
86
+ </div>
87
+ {pause.error ? <ApiError error={pause.error} title="Could not pause" /> : null}
88
+ <Card>
89
+ <CardHeader><CardTitle>Issues</CardTitle></CardHeader>
90
+ <CardContent className="grid gap-2">
91
+ {issues.error ? <ApiError error={issues.error} title="Could not load issues" onRetry={() => void issues.refetch()} /> : issues.loading && !issues.data ? <Skeleton className="h-16 w-full" /> : (issues.data ?? []).length === 0 ? (
92
+ <p className="text-muted-foreground text-sm">No issues yet.</p>
93
+ ) : (
94
+ <ul className="grid gap-1">
95
+ {(issues.data ?? []).map((i) => (
96
+ <li key={i.id}>
97
+ <Button variant={i.id === issueId ? "secondary" : "ghost"} size="sm" className="w-full justify-between" onClick={() => { setIssueId(i.id); onSelectIssue?.(current.id, i); }}>
98
+ <span>{issueTitle(i)}</span><span className="text-muted-foreground text-xs">{i.status}</span>
99
+ </Button>
100
+ </li>
101
+ ))}
102
+ </ul>
103
+ )}
104
+ <div><Button size="sm" onClick={() => setIssueId("new")}>New issue</Button></div>
105
+ </CardContent>
106
+ </Card>
107
+ {issueId !== undefined ? (
108
+ <Card>
109
+ <CardHeader><CardTitle>{issueId === "new" ? "New issue" : "Edit issue"}</CardTitle></CardHeader>
110
+ <CardContent>
111
+ <IssueForm key={String(issueId)} newsletterId={current.id} issueId={issueId === "new" ? undefined : issueId}
112
+ onSaved={(saved) => { void issues.refetch(); setIssueId(saved.id); }} onDeleted={() => { setIssueId(undefined); void issues.refetch(); }} />
113
+ </CardContent>
114
+ </Card>
115
+ ) : null}
116
+ </>
117
+ )}
118
+ </div>
119
+ <ConfirmSpendDialog
120
+ open={pending !== null}
121
+ tier="external"
122
+ title={pending?.kind === "activate" ? `Activate ${current?.name ?? "this newsletter"}?` : `Create the newsletter "${name.trim()}"?`}
123
+ description={<><p>{pending?.kind === "activate" ? "Once active, the scheduler sends any issue whose scheduled time has passed to real subscribers." : "This creates the newsletter container. Nothing is sent until you activate it and schedule an issue."}</p>{(create.error ?? activate.error) ? <ApiError error={create.error ?? activate.error} title="That did not work" /> : null}</>}
124
+ confirmLabel={pending?.kind === "activate" ? "Activate" : "Create"}
125
+ confirming={create.loading || activate.loading}
126
+ onCancel={() => setPending(null)}
127
+ onConfirm={() => void (pending?.kind === "activate" ? activate.mutate(undefined) : create.mutate(undefined))}
128
+ />
129
+ </div>
130
+ );
131
+ }
@@ -0,0 +1,93 @@
1
+ "use client";
2
+
3
+ /**
4
+ * NewsletterStats: how sent issues performed (delivered, opened, clicked, bounced, unsubscribed).
5
+ *
6
+ * SDK operations, in order:
7
+ * 1. listIssues (GET /newsletters/{id}/issues) read (sent rows carry inline KPIs)
8
+ * 2. getIssueAnalyticsKpis (GET .../issues/{issue_id}/analytics/kpis) read (fresh numbers for the selected issue)
9
+ * KPIs can lag: the API answers 200 with `stale: true` and an empty bucket when its read failed, which is different
10
+ * from "zero engagement yet". The panel says so instead of showing zeros. Open rates are approximate (image-blocking
11
+ * and privacy proxies) and are shown as a share of delivered mail.
12
+ */
13
+
14
+ import { useState } from "react";
15
+ import { Button, Card, CardContent, CardHeader, CardTitle, Skeleton, Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@graph8/sdk/ui";
16
+ import { ApiError } from "@/g8/components/api-error";
17
+ import { EmptyState } from "@/g8/components/empty-state";
18
+ import { g8Get } from "@/g8/lib/g8-client";
19
+ import { useG8Query } from "@/g8/hooks/use-g8-query";
20
+ import { issueTitle, percent } from "../lib/newsletter-types";
21
+ import type { Issue, IssueKpis } from "../lib/newsletter-types";
22
+
23
+ export interface NewsletterStatsProps {
24
+ newsletterId: string;
25
+ }
26
+
27
+ interface Kpis { stale?: boolean; last_updated_at?: string | null; kpis?: IssueKpis }
28
+
29
+ function Tile({ label, value, sub }: { label: string; value: number | undefined; sub?: string }) {
30
+ return (
31
+ <div className="rounded-md border p-3">
32
+ <p className="text-muted-foreground text-xs">{label}</p>
33
+ <p className="text-xl font-semibold">{(value ?? 0).toLocaleString()}</p>
34
+ {sub ? <p className="text-muted-foreground text-xs">{sub}</p> : null}
35
+ </div>
36
+ );
37
+ }
38
+
39
+ export function NewsletterStats({ newsletterId }: NewsletterStatsProps) {
40
+ const base = `/newsletters/${encodeURIComponent(newsletterId)}/issues`;
41
+ const issues = useG8Query((signal) => g8Get<Issue[]>(base, {}, { signal }).then((r) => r.data ?? []), [base]);
42
+ const [selected, setSelected] = useState<number | undefined>(undefined);
43
+ const detail = useG8Query((signal) => g8Get<Kpis>(`${base}/${selected}/analytics/kpis`, {}, { signal }).then((r) => r.data), [base, selected], { enabled: selected !== undefined });
44
+
45
+ if (issues.error) return <ApiError error={issues.error} title="Could not load issues" onRetry={() => void issues.refetch()} />;
46
+ if (issues.loading && !issues.data) return <Skeleton className="h-40 w-full" aria-busy="true" />;
47
+ const sent = (issues.data ?? []).filter((i) => i.status === "sent" || i.sent_at);
48
+ if (sent.length === 0) return <EmptyState title="Nothing sent yet" description="Statistics appear here after the first issue goes out." />;
49
+ const k = detail.data?.kpis;
50
+ return (
51
+ <div className="grid gap-4">
52
+ <Table>
53
+ <TableHeader><TableRow><TableHead>Issue</TableHead><TableHead>Sent</TableHead><TableHead>Delivered</TableHead><TableHead>Opened</TableHead><TableHead>Clicked</TableHead><TableHead /></TableRow></TableHeader>
54
+ <TableBody>
55
+ {sent.map((i) => (
56
+ <TableRow key={i.id}>
57
+ <TableCell>{issueTitle(i)}</TableCell>
58
+ <TableCell>{i.sent_at ? new Date(i.sent_at).toLocaleDateString() : ""}</TableCell>
59
+ <TableCell>{(i.kpis?.delivered ?? 0).toLocaleString()}</TableCell>
60
+ <TableCell>{percent(i.kpis?.opened, i.kpis?.delivered)}</TableCell>
61
+ <TableCell>{percent(i.kpis?.clicked, i.kpis?.delivered)}</TableCell>
62
+ <TableCell className="text-right"><Button size="sm" variant="ghost" onClick={() => setSelected(i.id)}>Details</Button></TableCell>
63
+ </TableRow>
64
+ ))}
65
+ </TableBody>
66
+ </Table>
67
+ {selected !== undefined ? (
68
+ <Card>
69
+ <CardHeader><CardTitle>Issue details</CardTitle></CardHeader>
70
+ <CardContent className="grid gap-3">
71
+ {detail.error ? <ApiError error={detail.error} title="Could not load the numbers" onRetry={() => void detail.refetch()} /> : detail.loading && !detail.data ? <Skeleton className="h-20 w-full" /> : detail.data?.stale ? (
72
+ <p role="alert" className="text-sm text-amber-700">The numbers could not be read just now. This is not zero engagement: try again in a minute.</p>
73
+ ) : (
74
+ <>
75
+ <div className="grid grid-cols-2 gap-2 sm:grid-cols-4">
76
+ <Tile label="Delivered" value={k?.delivered} sub={`of ${(k?.total ?? 0).toLocaleString()} queued`} />
77
+ <Tile label="Opened" value={k?.opened} sub={percent(k?.opened, k?.delivered)} />
78
+ <Tile label="Clicked" value={k?.clicked} sub={percent(k?.clicked, k?.delivered)} />
79
+ <Tile label="Bounced" value={k?.bounced} sub={percent(k?.bounced, k?.total)} />
80
+ <Tile label="Unsubscribed" value={k?.unsubscribed_attributed} />
81
+ <Tile label="Complaints" value={k?.complained} />
82
+ <Tile label="Skipped" value={k?.skipped} />
83
+ <Tile label="No activity" value={k?.no_activity} />
84
+ </div>
85
+ {detail.data?.last_updated_at ? <p className="text-muted-foreground text-xs">Updated {new Date(detail.data.last_updated_at).toLocaleString()}</p> : null}
86
+ </>
87
+ )}
88
+ </CardContent>
89
+ </Card>
90
+ ) : null}
91
+ </div>
92
+ );
93
+ }
@@ -0,0 +1,173 @@
1
+ "use client";
2
+
3
+ /**
4
+ * SubscriberTable: who is on the newsletter, plus add, import from a list, verify, unsubscribe and erase.
5
+ *
6
+ * SDK operations, in order:
7
+ * 1. listSubscribers (GET /newsletters/{id}/subscribers?page&page_size&search&status) read
8
+ * 2. addSubscriber (POST .../subscribers) external -> ConfirmSpendDialog (they will receive future issues)
9
+ * 3. listLists (GET /lists) + importSubscribersFromAudience (POST .../subscribers/import-audience) billable -> ConfirmSpendDialog (async; may verify addresses)
10
+ * 4. previewNewsletterSubscriberVerification (POST .../subscribers/verify/preview) read (free: how many, what it costs)
11
+ * 5. verifyNewsletterSubscribers (POST .../subscribers/verify) billable -> ConfirmSpendDialog with the previewed cost
12
+ * 6. unsubscribeSubscriber (PATCH .../subscribers/{id}/unsubscribe) external -> ConfirmSpendDialog (keeps the row and the opt-out)
13
+ * 7. deleteSubscriber (DELETE .../subscribers/{id}) destructive -> ConfirmSpendDialog (erases the opt-out record too)
14
+ * Prefer Unsubscribe over Delete: deleting loses the record that someone opted out, so a re-import could mail them again.
15
+ */
16
+
17
+ import { useState } from "react";
18
+ import { Button, Input, Skeleton, Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@graph8/sdk/ui";
19
+ import { ApiError } from "@/g8/components/api-error";
20
+ import { ConfirmSpendDialog } from "@/g8/components/confirm-spend-dialog";
21
+ import { EmptyState } from "@/g8/components/empty-state";
22
+ import { g8Delete, g8Get, g8Patch, g8Post } from "@/g8/lib/g8-client";
23
+ import { useG8Mutation } from "@/g8/hooks/use-g8-mutation";
24
+ import { useG8Query } from "@/g8/hooks/use-g8-query";
25
+ import type { Subscriber } from "../lib/newsletter-types";
26
+
27
+ export interface SubscriberTableProps {
28
+ /** The newsletter to show. Omit it (for example as a stack nav page) and a newsletter picker is shown instead. */
29
+ newsletterId?: string;
30
+ pageSize?: number;
31
+ }
32
+
33
+ type Action =
34
+ | { kind: "add" }
35
+ | { kind: "import"; listId: number; contacts?: number }
36
+ | { kind: "verify" }
37
+ | { kind: "unsubscribe"; subscriber: Subscriber }
38
+ | { kind: "delete"; subscriber: Subscriber };
39
+
40
+ interface Page { items: Subscriber[]; total: number }
41
+ interface ListOption { id: number; title: string; contact_count?: number }
42
+
43
+ function creditsOf(preview: unknown): number | undefined {
44
+ const p = (preview ?? {}) as Record<string, unknown>;
45
+ const value = p.estimated_credits ?? p.credits ?? p.cost ?? p.estimated_cost;
46
+ return typeof value === "number" ? value : undefined;
47
+ }
48
+
49
+ export function SubscriberTable({ newsletterId: given, pageSize = 25 }: SubscriberTableProps) {
50
+ const [picked, setPicked] = useState("");
51
+ const all = useG8Query((signal) => g8Get<Array<{ id: string; name: string }>>("/newsletters", {}, { signal }).then((r) => r.data ?? []), [], { enabled: !given });
52
+ const newsletterId = given ?? picked;
53
+ if (!newsletterId) {
54
+ if (all.error) return <ApiError error={all.error} title="Could not load newsletters" onRetry={() => void all.refetch()} />;
55
+ if (all.loading && !all.data) return <Skeleton className="h-9 w-64" aria-busy="true" />;
56
+ if ((all.data ?? []).length === 0) return <EmptyState title="No newsletters yet" description="Create a newsletter first (NewsletterEditor)." />;
57
+ return (
58
+ <select aria-label="Choose a newsletter" className="border-input bg-background h-9 max-w-sm rounded-md border px-2 text-sm" value="" onChange={(e) => setPicked(e.target.value)}>
59
+ <option value="" disabled>Choose a newsletter</option>
60
+ {(all.data ?? []).map((n) => <option key={n.id} value={n.id}>{n.name}</option>)}
61
+ </select>
62
+ );
63
+ }
64
+ return <SubscribersOf newsletterId={newsletterId} pageSize={pageSize} />;
65
+ }
66
+
67
+ function SubscribersOf({ newsletterId, pageSize }: { newsletterId: string; pageSize: number }) {
68
+ const base = `/newsletters/${encodeURIComponent(newsletterId)}/subscribers`;
69
+ const [page, setPage] = useState(1);
70
+ const [search, setSearch] = useState("");
71
+ const [query, setQuery] = useState("");
72
+ const [status, setStatus] = useState("");
73
+ const [draft, setDraft] = useState({ email: "", first_name: "", last_name: "" });
74
+ const [listId, setListId] = useState("");
75
+ const [action, setAction] = useState<Action | null>(null);
76
+ const subscribers = useG8Query<Page>(
77
+ (signal) => g8Get<Page>(base, { page, page_size: pageSize, search: query || undefined, status: status || undefined }, { signal }).then((r) => ({ items: r.data?.items ?? [], total: r.data?.total ?? 0 })),
78
+ [base, page, pageSize, query, status],
79
+ );
80
+ const lists = useG8Query((signal) => g8Get<ListOption[]>("/lists", { limit: 200 }, { signal }).then((r) => r.data ?? []), []);
81
+ const preview = useG8Mutation(async () => (await g8Post(`${base}/verify/preview`, {})).data);
82
+
83
+ const run = useG8Mutation(async (a: Action) => {
84
+ if (a.kind === "add") return g8Post(base, { email: draft.email.trim(), first_name: draft.first_name.trim() || undefined, last_name: draft.last_name.trim() || undefined });
85
+ if (a.kind === "import") return g8Post(`${base}/import-audience`, { audience_id: a.listId });
86
+ if (a.kind === "verify") return g8Post(`${base}/verify`, {});
87
+ if (a.kind === "unsubscribe") return g8Patch(`${base}/${a.subscriber.id}/unsubscribe`, {});
88
+ return g8Delete(`${base}/${a.subscriber.id}`);
89
+ }, { onSuccess: (_r, a) => { setAction(null); if (a.kind === "add") setDraft({ email: "", first_name: "", last_name: "" }); void subscribers.refetch(); } });
90
+
91
+ const copy = (a: Action | null): { tier: "external" | "billable" | "destructive"; title: string; note: string; label: string; credits?: number; affected?: number; recipients?: number } => {
92
+ switch (a?.kind) {
93
+ case "import": return { tier: "billable", title: "Import this list as subscribers?", note: "The import runs in the background. Only import people who agreed to hear from you; addresses may be verified, which spends credits.", label: "Import", recipients: a.contacts };
94
+ case "verify": return { tier: "billable", title: "Verify unverified addresses?", note: "Every unverified subscriber address is checked and charged per address.", label: "Verify", credits: creditsOf(preview.data) };
95
+ case "unsubscribe": return { tier: "external", title: `Unsubscribe ${a.subscriber.email}?`, note: "They stay in the list as unsubscribed and will not be mailed.", label: "Unsubscribe" };
96
+ case "delete": return { tier: "destructive", title: `Erase ${a.subscriber.email}?`, note: "This removes the person and the record that they opted out. Use only for an erasure request.", label: "Erase", affected: 1 };
97
+ default: return { tier: "external", title: `Add ${draft.email.trim()}?`, note: "They will receive future issues. Only add people who agreed to be mailed.", label: "Add subscriber", recipients: 1 };
98
+ }
99
+ };
100
+ const c = copy(action);
101
+ const items = subscribers.data?.items ?? [];
102
+ const total = subscribers.data?.total ?? 0;
103
+
104
+ return (
105
+ <div className="grid gap-4">
106
+ <form className="grid gap-2 sm:grid-cols-[1fr_8rem_8rem_auto]" onSubmit={(e) => { e.preventDefault(); if (draft.email.includes("@")) setAction({ kind: "add" }); }}>
107
+ <Input aria-label="Email" type="email" placeholder="Email" value={draft.email} onChange={(e) => setDraft({ ...draft, email: e.target.value })} />
108
+ <Input aria-label="First name" placeholder="First name" value={draft.first_name} onChange={(e) => setDraft({ ...draft, first_name: e.target.value })} />
109
+ <Input aria-label="Last name" placeholder="Last name" value={draft.last_name} onChange={(e) => setDraft({ ...draft, last_name: e.target.value })} />
110
+ <Button type="submit" disabled={!draft.email.includes("@")}>Add subscriber</Button>
111
+ </form>
112
+ <div className="flex flex-wrap items-end gap-2">
113
+ <select aria-label="Import from list" value={listId} onChange={(e) => setListId(e.target.value)} className="border-input bg-background h-9 rounded-md border px-2 text-sm">
114
+ <option value="">Import from a list...</option>
115
+ {(lists.data ?? []).map((l) => <option key={l.id} value={l.id}>{l.title}</option>)}
116
+ </select>
117
+ <Button variant="outline" disabled={!listId} onClick={() => setAction({ kind: "import", listId: Number(listId), contacts: lists.data?.find((l) => l.id === Number(listId))?.contact_count })}>Import list</Button>
118
+ <Button variant="outline" loading={preview.loading} onClick={() => void preview.mutate(undefined)}>Check verification cost</Button>
119
+ {preview.data ? <Button variant="outline" onClick={() => setAction({ kind: "verify" })}>Verify addresses</Button> : null}
120
+ </div>
121
+ {preview.data ? <pre role="status" className="rounded border p-2 text-xs">{JSON.stringify(preview.data)}</pre> : null}
122
+ {preview.error ? <ApiError error={preview.error} title="Could not estimate verification" /> : null}
123
+ <form className="flex gap-2" onSubmit={(e) => { e.preventDefault(); setPage(1); setQuery(search.trim()); }}>
124
+ <Input aria-label="Search subscribers" placeholder="Search by email or name" value={search} onChange={(e) => setSearch(e.target.value)} />
125
+ <select aria-label="Status" value={status} onChange={(e) => { setPage(1); setStatus(e.target.value); }} className="border-input bg-background h-9 rounded-md border px-2 text-sm">
126
+ <option value="">All</option><option value="subscribed">Subscribed</option><option value="unsubscribed">Unsubscribed</option>
127
+ </select>
128
+ <Button type="submit" variant="outline">Search</Button>
129
+ </form>
130
+ {subscribers.error ? <ApiError error={subscribers.error} title="Could not load subscribers" onRetry={() => void subscribers.refetch()} /> : subscribers.loading && !subscribers.data ? (
131
+ <div aria-busy="true" className="grid gap-2"><Skeleton className="h-9 w-full" /><Skeleton className="h-9 w-full" /></div>
132
+ ) : items.length === 0 ? <EmptyState title="No subscribers" description="Add someone above, or import one of your lists." /> : (
133
+ <Table>
134
+ <TableHeader><TableRow><TableHead>Email</TableHead><TableHead>Name</TableHead><TableHead>Status</TableHead><TableHead>Verification</TableHead><TableHead /></TableRow></TableHeader>
135
+ <TableBody>
136
+ {items.map((s) => (
137
+ <TableRow key={s.id}>
138
+ <TableCell>{s.email}</TableCell>
139
+ <TableCell>{[s.first_name, s.last_name].filter(Boolean).join(" ")}</TableCell>
140
+ <TableCell>{s.status}</TableCell>
141
+ <TableCell>{s.verification_status ?? ""}</TableCell>
142
+ <TableCell className="text-right">
143
+ {s.status !== "unsubscribed" ? <Button size="sm" variant="ghost" onClick={() => setAction({ kind: "unsubscribe", subscriber: s })}>Unsubscribe</Button> : null}
144
+ <Button size="sm" variant="ghost" onClick={() => setAction({ kind: "delete", subscriber: s })}>Erase</Button>
145
+ </TableCell>
146
+ </TableRow>
147
+ ))}
148
+ </TableBody>
149
+ </Table>
150
+ )}
151
+ <div className="flex items-center justify-between text-sm">
152
+ <span className="text-muted-foreground">{total.toLocaleString()} subscribers</span>
153
+ <span>
154
+ <Button size="sm" variant="ghost" disabled={page <= 1} onClick={() => setPage(page - 1)}>Previous</Button>
155
+ <Button size="sm" variant="ghost" disabled={page * pageSize >= total} onClick={() => setPage(page + 1)}>Next</Button>
156
+ </span>
157
+ </div>
158
+ <ConfirmSpendDialog
159
+ open={action !== null}
160
+ tier={c.tier}
161
+ title={c.title}
162
+ estimatedCredits={c.credits}
163
+ recipients={c.recipients}
164
+ affected={c.affected}
165
+ description={<><p>{c.note}</p>{run.error ? <ApiError error={run.error} title="That did not work" /> : null}</>}
166
+ confirmLabel={c.label}
167
+ confirming={run.loading}
168
+ onCancel={() => setAction(null)}
169
+ onConfirm={() => { if (action) void run.mutate(action); }}
170
+ />
171
+ </div>
172
+ );
173
+ }
@@ -0,0 +1,97 @@
1
+ "use client";
2
+
3
+ /**
4
+ * Types and helpers for the newsletter block.
5
+ *
6
+ * Responses are open dicts in the contract (the app adds columns freely), so every field here is optional and the
7
+ * components render what is present. SENDING IS EXTERNAL: every call that can reach a subscriber's inbox is tier
8
+ * `external` and must be confirmed with the subscriber count (see IssueScheduler).
9
+ */
10
+
11
+ export interface Newsletter {
12
+ id: string;
13
+ name: string;
14
+ slug?: string;
15
+ status?: string;
16
+ goal?: string | null;
17
+ audience_list_id?: string | number | null;
18
+ }
19
+
20
+ export interface IssueKpis {
21
+ total?: number;
22
+ delivered?: number;
23
+ opened?: number;
24
+ clicked?: number;
25
+ bounced?: number;
26
+ complained?: number;
27
+ unsubscribed_attributed?: number;
28
+ skipped?: number;
29
+ no_activity?: number;
30
+ }
31
+
32
+ export interface Issue {
33
+ id: number;
34
+ newsletter_id?: string;
35
+ subject?: string | null;
36
+ status?: string;
37
+ sent_at?: string | null;
38
+ scheduled_at?: string | null;
39
+ html_body?: string | null;
40
+ plain_text?: string | null;
41
+ preview_text?: string | null;
42
+ sender_name?: string | null;
43
+ sender_email?: string | null;
44
+ reply_to?: string | null;
45
+ kpis?: IssueKpis | null;
46
+ }
47
+
48
+ export interface Subscriber {
49
+ id: number;
50
+ email: string;
51
+ first_name?: string | null;
52
+ last_name?: string | null;
53
+ status?: string;
54
+ verification_status?: string | null;
55
+ created_at?: string;
56
+ }
57
+
58
+ export interface SendState {
59
+ active?: boolean;
60
+ send?: { status?: string; sent_count?: number; total_recipients?: number; eligible_recipients?: number; failed_count?: number } | null;
61
+ }
62
+
63
+ /** Only drafts can be edited or deleted; a sent issue is a record of mail real people received. */
64
+ export function isDraft(issue: Pick<Issue, "status"> | undefined): boolean {
65
+ return !issue?.status || issue.status === "draft";
66
+ }
67
+
68
+ export function percent(part: number | undefined, whole: number | undefined): string {
69
+ if (!whole) return "0%";
70
+ return `${Math.round(((part ?? 0) / whole) * 1000) / 10}%`;
71
+ }
72
+
73
+ /** `datetime-local` text ("2026-11-01T09:00") to the ISO instant the API wants. Returns undefined when empty or invalid. */
74
+ export function toIsoInstant(local: string): string | undefined {
75
+ if (!local) return undefined;
76
+ const d = new Date(local);
77
+ return Number.isNaN(d.getTime()) ? undefined : d.toISOString();
78
+ }
79
+
80
+ /** A generation that has not finished has PARTIAL html: never send it. */
81
+ export function generationInProgress(status: unknown): boolean {
82
+ const word = typeof status === "string" ? status : ((status as { status?: string } | null | undefined)?.status ?? "");
83
+ return /pending|queued|running|generating|in_progress|processing/i.test(word);
84
+ }
85
+
86
+ export function issueTitle(issue: Pick<Issue, "subject" | "id">): string {
87
+ return issue.subject?.trim() || `Untitled issue ${issue.id}`;
88
+ }
89
+
90
+ /** Pause is offered while a send is moving; Resume while it is paused (or pausing). */
91
+ export function sendIsMoving(status: string | undefined): boolean {
92
+ return /running|queued|resuming|sending|in_progress/i.test(status ?? "");
93
+ }
94
+
95
+ export function sendIsPaused(status: string | undefined): boolean {
96
+ return /paus/i.test(status ?? "");
97
+ }
@@ -0,0 +1,27 @@
1
+ /**
2
+ * @title Create a newsletter, draft its first issue, and send yourself a test
3
+ * @when Setting up a newsletter before any subscriber exists. Nothing here reaches a subscriber: creating,
4
+ * drafting and testing are safe; activating the newsletter (last step) is what lets the scheduler mail due issues,
5
+ * so it is left to a person.
6
+ */
7
+ import { g8 } from "@graph8/sdk";
8
+
9
+ g8.init({ apiKey: process.env.G8_API_KEY! });
10
+
11
+ const newsletter = await g8.newsletter.create({ name: "Product updates", goal: "Tell customers what shipped each month" });
12
+ console.log("newsletter", newsletter.id);
13
+
14
+ // The issue id is a NUMBER in the API, the hand-written client takes it as a string.
15
+ const issue = await g8.newsletter.createIssue(newsletter.id, {
16
+ subject: "What shipped in October",
17
+ preview_text: "Three things you asked for",
18
+ html_body: "<h1>October</h1><p>Here is what shipped.</p>",
19
+ sender_name: "The graph8 team",
20
+ });
21
+ console.log("draft issue", issue.id, issue.status);
22
+
23
+ // ONE email to ONE address you own.
24
+ await g8.newsletter.sendTestEmail(newsletter.id, String(issue.id), "you@example.com");
25
+
26
+ // Do not call activate from an agent without the owner's say-so: it is launch-class.
27
+ // await g8.api.newsletter.activateNewsletter({ path: { newsletter_id: newsletter.id } });
@@ -0,0 +1,43 @@
1
+ /**
2
+ * @title Import a list as subscribers, then send an issue and follow it to the end
3
+ * @when A one-off broadcast to an existing audience. Import is BILLABLE if auto_verify_on_import is on and the
4
+ * send is EXTERNAL (real inboxes): count first, confirm the number with the owner, send once with an idempotency
5
+ * key so a retry cannot double-send.
6
+ */
7
+ import { g8 } from "@graph8/sdk";
8
+
9
+ g8.init({ apiKey: process.env.G8_API_KEY! });
10
+
11
+ export async function broadcast(newsletterId: string, issueId: number, audienceListId: number): Promise<void> {
12
+ const path = { newsletter_id: newsletterId };
13
+
14
+ // 1. Import the list. The response only acknowledges the job (asynchronous).
15
+ await g8.api.newsletter.importSubscribersFromAudience({ path, body: { audience_id: audienceListId } });
16
+
17
+ // 2. Wait until the subscriber count stops growing, then read the number you are about to mail.
18
+ let last = -1;
19
+ let total = 0;
20
+ for (let i = 0; i < 20 && total !== last; i += 1) {
21
+ last = total;
22
+ await new Promise((r) => setTimeout(r, 3000));
23
+ const page = await g8.api.newsletter.listSubscribers({ path, query: { status: "subscribed", page_size: 1 } });
24
+ total = (page.data as { total?: number }).total ?? 0;
25
+ }
26
+ console.log(`About to mail ${total} subscribers. Stop here unless the owner approved that number.`);
27
+
28
+ // 3. Do not send an AI issue that is still generating: it would mail the partial version.
29
+ const gen = await g8.api.newsletter.getIssueGenerationStatus({ path: { ...path, issue_id: issueId } });
30
+ if (/pending|running|generating|queued/i.test(String((gen.data as { status?: string }).status ?? ""))) throw new Error("Issue is still being generated");
31
+
32
+ // 4. Send (202 = queued, not delivered). The idempotency key makes a retry safe.
33
+ await g8.api.newsletter.sendIssue({ path: { ...path, issue_id: issueId }, body: {} }, { idempotencyKey: `send-${newsletterId}-${issueId}` });
34
+
35
+ // 5. Follow it: `active` turns false when the worker is done.
36
+ for (let i = 0; i < 100; i += 1) {
37
+ const status = await g8.api.newsletter.getSendStatus({ path: { ...path, issue_id: issueId } });
38
+ const data = status.data as { active?: boolean; send?: { status?: string } };
39
+ console.log(data.send?.status ?? "idle");
40
+ if (data.active === false) break;
41
+ await new Promise((r) => setTimeout(r, 5000));
42
+ }
43
+ }
@@ -0,0 +1,26 @@
1
+ /**
2
+ * @title Report how every sent issue performed
3
+ * @when A weekly digest or a dashboard backend. Uses the API's field names (delivered, opened, clicked), which differ
4
+ * from the hand-written g8.newsletter.issueKpis types (opens, clicks). A response with stale=true means the read
5
+ * failed, NOT that nobody opened the mail.
6
+ */
7
+ import { g8 } from "@graph8/sdk";
8
+
9
+ g8.init({ apiKey: process.env.G8_API_KEY! });
10
+
11
+ export async function report(newsletterId: string): Promise<Array<{ issue: number; subject: string; delivered: number; openRate: number; clickRate: number }>> {
12
+ const issues = await g8.api.newsletter.listIssues({ path: { newsletter_id: newsletterId } });
13
+ const rows: Array<{ issue: number; subject: string; delivered: number; openRate: number; clickRate: number }> = [];
14
+ for (const issue of issues.data as Array<{ id: number; subject?: string; status?: string }>) {
15
+ if (issue.status !== "sent") continue;
16
+ const res = await g8.api.newsletter.getIssueAnalyticsKpis({ path: { newsletter_id: newsletterId, issue_id: issue.id } });
17
+ const data = res.data as { stale?: boolean; kpis: { delivered?: number; opened?: number; clicked?: number } };
18
+ if (data.stale) {
19
+ console.warn(`issue ${issue.id}: numbers unavailable right now, skipping (not zero)`);
20
+ continue;
21
+ }
22
+ const delivered = data.kpis.delivered ?? 0;
23
+ rows.push({ issue: issue.id, subject: issue.subject ?? "", delivered, openRate: delivered ? (data.kpis.opened ?? 0) / delivered : 0, clickRate: delivered ? (data.kpis.clicked ?? 0) / delivered : 0 });
24
+ }
25
+ return rows;
26
+ }