@graph8/sdk 0.308.1 → 0.357.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (654) 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 +1440 -21
  390. package/dist/index.d.ts +1440 -21
  391. package/dist/index.js +42 -3
  392. package/dist/index.js.map +1 -1
  393. package/dist/index.mjs +42 -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 +1340 -18
  400. package/dist/react.d.ts +1340 -18
  401. package/dist/react.js +41 -2
  402. package/dist/react.js.map +1 -1
  403. package/dist/react.mjs +41 -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 +6 -3
  611. package/docs/api/appObjects.d.ts +1 -1
  612. package/docs/api/appObjects.md +1 -1
  613. package/docs/api/by-path.md +29 -0
  614. package/docs/api/objects.d.ts +72 -5
  615. package/docs/api/objects.md +84 -7
  616. package/docs/api/processLists.d.ts +247 -0
  617. package/docs/api/processLists.md +409 -0
  618. package/docs/api/reports.d.ts +476 -0
  619. package/docs/api/reports.md +539 -0
  620. package/docs/api/scheduleBuy.d.ts +12 -8
  621. package/docs/api/scheduleBuy.md +13 -10
  622. package/docs/api/seo.d.ts +406 -3
  623. package/docs/api/seo.md +505 -1
  624. package/docs/api/workspaceBackup.d.ts +324 -0
  625. package/docs/api/workspaceBackup.md +427 -0
  626. package/package.json +174 -5
  627. package/stacks/6sense.json +46 -0
  628. package/stacks/README.md +20 -0
  629. package/stacks/apollo.json +128 -0
  630. package/stacks/clay.json +59 -0
  631. package/stacks/gong.json +72 -0
  632. package/stacks/hubspot.json +162 -0
  633. package/stacks/intercom.json +53 -0
  634. package/stacks/mailchimp.json +71 -0
  635. package/stacks/outreach.json +86 -0
  636. package/stacks/salesforce.json +94 -0
  637. package/stacks/stack.schema.json +32 -0
  638. package/stacks/starter.json +22 -0
  639. package/styles/styles.css +2 -0
  640. package/styles/theme.css +181 -0
  641. package/styles/tokens.css +284 -0
  642. package/templates/next-app/README.md.tmpl +35 -0
  643. package/templates/next-app/_pages/redirect-page.tsx.tmpl +5 -0
  644. package/templates/next-app/_pages/route-page.tsx.tmpl +9 -0
  645. package/templates/next-app/app/globals.css.tmpl +8 -0
  646. package/templates/next-app/app/layout.tsx.tmpl +19 -0
  647. package/templates/next-app/components/app-shell.tsx.tmpl +34 -0
  648. package/templates/next-app/dot-env.local.example.tmpl +6 -0
  649. package/templates/next-app/dot-gitignore.tmpl +7 -0
  650. package/templates/next-app/lib/nav.ts.tmpl +2 -0
  651. package/templates/next-app/next.config.mjs.tmpl +4 -0
  652. package/templates/next-app/package.json.tmpl +25 -0
  653. package/templates/next-app/postcss.config.mjs.tmpl +4 -0
  654. package/templates/next-app/tsconfig.json.tmpl +21 -0
@@ -0,0 +1,163 @@
1
+ "use client";
2
+
3
+ /**
4
+ * ContentLibrary: the content inventory (blogs, website pages, posts) with filters, add, edit and SEO enrichment.
5
+ *
6
+ * SDK operations, in the order the screen uses them:
7
+ * 1. listContentPages GET /content/pages?content_type&content_status&limit&offset (read)
8
+ * 2. createContentPage POST /content/pages (write; registers a URL)
9
+ * 3. getContentPage GET /content/pages/{id} (read; loads the editor)
10
+ * 4. updateContentPage PATCH /content/pages/{id} (write)
11
+ * 5. enrichContentPage POST /content/pages/{id}/enrich-seo (billable)
12
+ * 6. deleteContentPage DELETE /content/pages/{id} (destructive)
13
+ *
14
+ * Enrich and delete go through ConfirmSpendDialog. Delete is refused by the API when a calendar entry or
15
+ * grid row still references the page; the error is shown as-is. The API's `force=true` override is deliberately not offered here.
16
+ */
17
+
18
+ import { useEffect, useState } from "react";
19
+ import { Button, Input, Label, Sheet, SheetContent, SheetDescription, SheetHeader, SheetTitle, Skeleton, Table, TableBody, TableCell, TableHead, TableHeader, TableRow, Textarea } from "@graph8/sdk/ui";
20
+ import { g8Delete, g8Get, g8Patch, g8Post } from "@/g8/lib/g8-client";
21
+ import { useG8Query } from "@/g8/hooks/use-g8-query";
22
+ import { useG8Mutation } from "@/g8/hooks/use-g8-mutation";
23
+ import { ApiError } from "@/g8/components/api-error";
24
+ import { EmptyState } from "@/g8/components/empty-state";
25
+ import { ConfirmSpendDialog } from "@/g8/components/confirm-spend-dialog";
26
+ import { asRec, num, rowsOf, text } from "../lib/studio";
27
+ import type { ContentPageRow } from "../lib/studio";
28
+
29
+ export interface ContentLibraryProps {
30
+ pageSize?: number;
31
+ className?: string;
32
+ }
33
+
34
+ const TYPES = ["", "website", "blog", "linkedin", "newsletter"];
35
+ const STATUSES = ["", "idea", "in_progress", "done", "published", "rejected"];
36
+ const label = (v: string) => (v ? v.replace(/_/g, " ") : "all");
37
+
38
+ type Action = { kind: "enrich" | "delete"; page: ContentPageRow };
39
+
40
+ function Select({ id, value, options, onChange }: { id: string; value: string; options: string[]; onChange: (v: string) => void }) {
41
+ return (
42
+ <select id={id} aria-label={id} value={value} onChange={(e) => onChange(e.target.value)} className="border-input bg-background h-9 rounded-md border px-2 text-sm capitalize">
43
+ {options.map((o) => <option key={o} value={o}>{label(o)}</option>)}
44
+ </select>
45
+ );
46
+ }
47
+
48
+ function EditSheet({ pageId, onClose, onSaved }: { pageId: string; onClose: () => void; onSaved: () => void }) {
49
+ const detail = useG8Query((signal) => g8Get(`/content/pages/${encodeURIComponent(pageId)}`, undefined, { signal }), [pageId]);
50
+ const [form, setForm] = useState({ title: "", primary_keyword: "", content_status: "idea", body_content: "" });
51
+ const d = asRec(detail.data?.data);
52
+ useEffect(() => {
53
+ setForm({ title: text(d.title), primary_keyword: text(d.primary_keyword), content_status: text(d.content_status, "idea"), body_content: text(d.body_content) });
54
+ }, [detail.data]);
55
+ const save = useG8Mutation(() => g8Patch(`/content/pages/${encodeURIComponent(pageId)}`, form), { onSuccess: () => { onSaved(); onClose(); } });
56
+ return (
57
+ <Sheet open onOpenChange={(open) => { if (!open) onClose(); }}>
58
+ <SheetContent className="w-full overflow-y-auto sm:max-w-xl">
59
+ <SheetHeader>
60
+ <SheetTitle>Edit content</SheetTitle>
61
+ <SheetDescription>{text(d.url)}</SheetDescription>
62
+ </SheetHeader>
63
+ <div className="space-y-3 px-4 pb-4">
64
+ {detail.error ? <ApiError error={detail.error} onRetry={() => void detail.refetch()} /> : null}
65
+ {detail.loading && !detail.data ? <Skeleton className="h-32 w-full" /> : (
66
+ <>
67
+ <div className="space-y-1"><Label htmlFor="cl-title">Title</Label><Input id="cl-title" value={form.title} onChange={(e) => setForm({ ...form, title: e.target.value })} /></div>
68
+ <div className="space-y-1"><Label htmlFor="cl-kw">Primary keyword</Label><Input id="cl-kw" value={form.primary_keyword} onChange={(e) => setForm({ ...form, primary_keyword: e.target.value })} /></div>
69
+ <div className="space-y-1"><Label htmlFor="cl-status">Status</Label><Select id="cl-status" value={form.content_status} options={STATUSES.slice(1)} onChange={(v) => setForm({ ...form, content_status: v })} /></div>
70
+ <div className="space-y-1"><Label htmlFor="cl-body">Body</Label><Textarea id="cl-body" rows={10} value={form.body_content} onChange={(e) => setForm({ ...form, body_content: e.target.value })} /></div>
71
+ {save.error ? <ApiError error={save.error} title="Could not save" /> : null}
72
+ <Button loading={save.loading} onClick={() => void save.mutate(undefined)}>Save</Button>
73
+ </>
74
+ )}
75
+ </div>
76
+ </SheetContent>
77
+ </Sheet>
78
+ );
79
+ }
80
+
81
+ export function ContentLibrary({ pageSize = 25, className }: ContentLibraryProps) {
82
+ const [type, setType] = useState("");
83
+ const [status, setStatus] = useState("");
84
+ const [offset, setOffset] = useState(0);
85
+ const [url, setUrl] = useState("");
86
+ const [editing, setEditing] = useState<string | null>(null);
87
+ const [action, setAction] = useState<Action | null>(null);
88
+
89
+ const list = useG8Query(
90
+ (signal) => g8Get("/content/pages", { content_type: type || undefined, content_status: status || undefined, is_owned: true, limit: pageSize, offset }, { signal }),
91
+ [type, status, offset, pageSize],
92
+ );
93
+ const add = useG8Mutation((u: string) => g8Post("/content/pages", { url: u, content_type: type || "website" }), { onSuccess: () => { setUrl(""); void list.refetch(); } });
94
+ const run = useG8Mutation(
95
+ (a: Action) => (a.kind === "enrich" ? g8Post(`/content/pages/${encodeURIComponent(a.page.id)}/enrich-seo`) : g8Delete(`/content/pages/${encodeURIComponent(a.page.id)}`)),
96
+ { onSuccess: () => { setAction(null); void list.refetch(); } },
97
+ );
98
+
99
+ const data = asRec(list.data?.data);
100
+ const pages = rowsOf<ContentPageRow>(data, "pages");
101
+ const total = num(data.total) ?? pages.length;
102
+ // Deleting the last row of the last page leaves the offset past the end; step back to the previous page.
103
+ useEffect(() => { if (!list.loading && !list.error && pages.length === 0 && offset > 0) setOffset(Math.max(0, offset - pageSize)); }, [list.loading, list.error, pages.length, offset, pageSize]);
104
+ const name = (p: ContentPageRow) => p.title || p.url;
105
+
106
+ return (
107
+ <div className={className}>
108
+ <div className="mb-4 flex flex-wrap items-center gap-2">
109
+ <Select id="Content type" value={type} options={TYPES} onChange={(v) => { setType(v); setOffset(0); }} />
110
+ <Select id="Content status" value={status} options={STATUSES} onChange={(v) => { setStatus(v); setOffset(0); }} />
111
+ <div className="ml-auto flex gap-2">
112
+ <Input aria-label="Page URL" placeholder="https://example.com/blog/post" value={url} onChange={(e) => setUrl(e.target.value)} className="w-72" />
113
+ <Button disabled={!url.trim()} loading={add.loading} onClick={() => void add.mutate(url.trim())}>Add page</Button>
114
+ </div>
115
+ </div>
116
+ {list.error ? <ApiError error={list.error} onRetry={() => void list.refetch()} /> : null}
117
+ {add.error ? <ApiError error={add.error} title="Could not add that page" /> : null}
118
+ {run.error ? <ApiError error={run.error} title="That action failed" /> : null}
119
+
120
+ {list.loading && !list.data ? (
121
+ <div className="space-y-2" aria-busy="true">{[0, 1, 2].map((i) => <Skeleton key={i} className="h-10 w-full" />)}</div>
122
+ ) : !list.error && pages.length === 0 ? (
123
+ <EmptyState title="No content yet" description="Add the URL of a page you publish, or import a sitemap, to start tracking it." />
124
+ ) : (
125
+ <Table>
126
+ <TableHeader>
127
+ <TableRow><TableHead>Title</TableHead><TableHead>Type</TableHead><TableHead>Status</TableHead><TableHead>SEO score</TableHead><TableHead className="text-right">Actions</TableHead></TableRow>
128
+ </TableHeader>
129
+ <TableBody>
130
+ {pages.map((p) => (
131
+ <TableRow key={p.id}>
132
+ <TableCell><button type="button" className="text-left font-medium hover:underline" onClick={() => setEditing(p.id)}>{name(p)}</button><div className="text-muted-foreground text-xs">{p.url}</div></TableCell>
133
+ <TableCell className="capitalize">{label(text(p.content_type, "website"))}</TableCell>
134
+ <TableCell className="capitalize">{label(text(p.content_status, "idea"))}</TableCell>
135
+ <TableCell>{p.seo_score ?? "-"}</TableCell>
136
+ <TableCell className="space-x-1 text-right">
137
+ <Button size="sm" variant="outline" onClick={() => setAction({ kind: "enrich", page: p })}>Enrich SEO</Button>
138
+ <Button size="sm" variant="ghost" aria-label={`Delete ${name(p)}`} onClick={() => setAction({ kind: "delete", page: p })}>Delete</Button>
139
+ </TableCell>
140
+ </TableRow>
141
+ ))}
142
+ </TableBody>
143
+ </Table>
144
+ )}
145
+ <div className="mt-3 flex items-center justify-between text-sm">
146
+ <span className="text-muted-foreground">{total} {total === 1 ? "item" : "items"}</span>
147
+ <div className="flex gap-2">
148
+ <Button size="sm" variant="outline" disabled={offset === 0} onClick={() => setOffset(Math.max(0, offset - pageSize))}>Previous</Button>
149
+ <Button size="sm" variant="outline" disabled={offset + pageSize >= total} onClick={() => setOffset(offset + pageSize)}>Next</Button>
150
+ </div>
151
+ </div>
152
+
153
+ {editing ? <EditSheet pageId={editing} onClose={() => setEditing(null)} onSaved={() => void list.refetch()} /> : null}
154
+ {action ? (
155
+ <ConfirmSpendDialog open tier={action.kind === "enrich" ? "billable" : "destructive"}
156
+ title={action.kind === "enrich" ? `Enrich SEO for "${name(action.page)}"` : `Delete "${name(action.page)}"`}
157
+ description={action.kind === "enrich" ? "Pulls rankings, backlinks and keyword data from a paid provider." : "The page leaves your content inventory."}
158
+ confirmLabel={action.kind === "enrich" ? "Enrich" : "Delete"} affected={action.kind === "delete" ? 1 : undefined}
159
+ confirming={run.loading} onConfirm={() => void run.mutate(action)} onCancel={() => setAction(null)} />
160
+ ) : null}
161
+ </div>
162
+ );
163
+ }
@@ -0,0 +1,253 @@
1
+ "use client";
2
+
3
+ /**
4
+ * PageEditor (lite): edit a landing page as structured sections, not a WYSIWYG canvas.
5
+ *
6
+ * Each section is a card; every text field of its `content` is an input. Saving a section sends the whole
7
+ * section content back (the API replaces it). Freeform pages have no sections: the editor shows the page
8
+ * details and the preview link, and edits to the HTML itself go through your own tooling or the graph8 app.
9
+ *
10
+ * SDK operations, in the order the screen uses them:
11
+ * 1. getLandingPage GET /landing-pages/{id} (read; sections, status, URL)
12
+ * 2. updateLandingPage PATCH /landing-pages/{id} (write; name, slug, description)
13
+ * 3. updateLandingPageSection PATCH /landing-pages/{id}/sections/{idx} (write; one section's content)
14
+ * 4. createLandingPagePreviewLink GET /landing-pages/{id}/preview (read; opens a private preview)
15
+ * 5. listLandingPageVersions GET /landing-pages/{id}/versions (read)
16
+ * 6. restoreLandingPageVersion POST /landing-pages/{id}/versions/{n}/restore (write; snapshots first)
17
+ * 7. publishLandingPage POST /landing-pages/{id}/publish (external: public URL)
18
+ * 8. unpublishLandingPage POST /landing-pages/{id}/unpublish (external)
19
+ *
20
+ * Publish and unpublish go through ConfirmSpendDialog. A published page keeps serving its published HTML
21
+ * until it is published again, so saving a section does not change the live page.
22
+ */
23
+
24
+ import { useState } from "react";
25
+ import { Button, Card, CardContent, CardHeader, CardTitle, Input, Label, Skeleton, Textarea } from "@graph8/sdk/ui";
26
+ import { g8Get, g8Patch, g8Post } from "@/g8/lib/g8-client";
27
+ import { useG8Query } from "@/g8/hooks/use-g8-query";
28
+ import { useG8Mutation } from "@/g8/hooks/use-g8-mutation";
29
+ import { ApiError } from "@/g8/components/api-error";
30
+ import { ConfirmSpendDialog } from "@/g8/components/confirm-spend-dialog";
31
+ import { asRec, liveUrl, rowsOf, shortDate, text } from "../lib/studio";
32
+ import type { LandingSection, Rec } from "../lib/studio";
33
+
34
+ export interface PageEditorProps {
35
+ pageId: string;
36
+ /** Called after a successful save of any part of the page. */
37
+ onSaved?: () => void;
38
+ className?: string;
39
+ }
40
+
41
+ function isScalar(v: unknown): v is string | number {
42
+ return typeof v === "string" || typeof v === "number";
43
+ }
44
+
45
+ interface SectionCardProps {
46
+ index: number;
47
+ section: LandingSection;
48
+ pageId: string;
49
+ onSaved: () => void;
50
+ }
51
+
52
+ /**
53
+ * One section: the scalar fields of its content are editable, anything nested is kept untouched.
54
+ * Only the fields the person changed are held as `edits`, shown over the server copy. A refetch (another section
55
+ * saved, a restore, a publish) therefore never wipes unsaved typing; `edits` is cleared only by this card's own save.
56
+ */
57
+ function SectionCard({ index, section, pageId, onSaved }: SectionCardProps) {
58
+ const content: Rec = asRec(section.content);
59
+ const [edits, setEdits] = useState<Rec>({});
60
+ const fields = Object.keys(content).filter((k) => isScalar(content[k]));
61
+ const dirty = fields.some((k) => k in edits && edits[k] !== content[k]);
62
+ const save = useG8Mutation(
63
+ () => g8Patch(`/landing-pages/${encodeURIComponent(pageId)}/sections/${index}`, { content: { ...content, ...edits } }),
64
+ { onSuccess: () => { setEdits({}); onSaved(); } },
65
+ );
66
+ return (
67
+ <Card data-testid={`section-${index}`}>
68
+ <CardHeader>
69
+ <CardTitle className="text-sm capitalize">
70
+ {index + 1}. {text(section.type, "section").replace(/[_-]/g, " ")}
71
+ </CardTitle>
72
+ </CardHeader>
73
+ <CardContent className="space-y-3">
74
+ {fields.length === 0 ? <p className="text-muted-foreground text-sm">This section has no plain text fields to edit here.</p> : null}
75
+ {fields.map((key) => {
76
+ const id = `s${index}-${key}`;
77
+ const current = key in edits ? edits[key] : content[key];
78
+ const numeric = typeof content[key] === "number";
79
+ const value = text(current);
80
+ if (numeric) {
81
+ // Numbers stay numbers: the endpoint replaces the content, and a "3" where a 3 was breaks renderers.
82
+ return (
83
+ <div key={key} className="space-y-1">
84
+ <Label htmlFor={id}>{key.replace(/_/g, " ")}</Label>
85
+ <Input id={id} type="number" value={value} onChange={(e) => setEdits({ ...edits, [key]: e.target.value === "" ? 0 : Number(e.target.value) })} />
86
+ </div>
87
+ );
88
+ }
89
+ const onChange = (e: { target: { value: string } }) => setEdits({ ...edits, [key]: e.target.value });
90
+ return (
91
+ <div key={key} className="space-y-1">
92
+ <Label htmlFor={id}>{key.replace(/_/g, " ")}</Label>
93
+ {value.length > 80 ? <Textarea id={id} rows={3} value={value} onChange={onChange} /> : <Input id={id} value={value} onChange={onChange} />}
94
+ </div>
95
+ );
96
+ })}
97
+ {save.error ? <ApiError error={save.error} title="Could not save this section" /> : null}
98
+ <Button size="sm" disabled={!dirty} loading={save.loading} onClick={() => void save.mutate(undefined)}>
99
+ Save section
100
+ </Button>
101
+ </CardContent>
102
+ </Card>
103
+ );
104
+ }
105
+
106
+ type Meta = Partial<Record<"name" | "slug" | "description", string>>;
107
+
108
+ /** Same idea as SectionCard: only changed fields are held, so a refetch never wipes them. Keyed by page id only. */
109
+ function DetailsCard({ pageId, data, onSaved }: { pageId: string; data: Rec; onSaved: () => void }) {
110
+ const [edits, setEdits] = useState<Meta>({});
111
+ const save = useG8Mutation(() => g8Patch(`/landing-pages/${encodeURIComponent(pageId)}`, edits), { onSuccess: () => { setEdits({}); onSaved(); } });
112
+ const val = (k: keyof Meta) => edits[k] ?? text(data[k]);
113
+ return (
114
+ <Card className="mb-4">
115
+ <CardHeader>
116
+ <CardTitle className="text-sm">Page details</CardTitle>
117
+ </CardHeader>
118
+ <CardContent className="grid gap-3 sm:grid-cols-2">
119
+ <div className="space-y-1">
120
+ <Label htmlFor="pg-name">Name</Label>
121
+ <Input id="pg-name" value={val("name")} onChange={(e) => setEdits({ ...edits, name: e.target.value })} />
122
+ </div>
123
+ <div className="space-y-1">
124
+ <Label htmlFor="pg-slug">Slug</Label>
125
+ <Input id="pg-slug" value={val("slug")} onChange={(e) => setEdits({ ...edits, slug: e.target.value })} />
126
+ </div>
127
+ <div className="space-y-1 sm:col-span-2">
128
+ <Label htmlFor="pg-desc">Description</Label>
129
+ <Textarea id="pg-desc" rows={2} value={val("description")} onChange={(e) => setEdits({ ...edits, description: e.target.value })} />
130
+ </div>
131
+ {save.error ? (
132
+ <div className="sm:col-span-2">
133
+ <ApiError error={save.error} title="Could not save the details" />
134
+ </div>
135
+ ) : null}
136
+ <div>
137
+ <Button size="sm" disabled={Object.keys(edits).length === 0} loading={save.loading} onClick={() => void save.mutate(undefined)}>
138
+ Save details
139
+ </Button>
140
+ </div>
141
+ </CardContent>
142
+ </Card>
143
+ );
144
+ }
145
+
146
+ export function PageEditor({ pageId, onSaved, className }: PageEditorProps) {
147
+ const base = `/landing-pages/${encodeURIComponent(pageId)}`;
148
+ const page = useG8Query((signal) => g8Get(base, undefined, { signal }), [pageId]);
149
+ const versions = useG8Query((signal) => g8Get(`${base}/versions`, undefined, { signal }), [pageId]);
150
+ const [confirm, setConfirm] = useState<"publish" | "unpublish" | null>(null);
151
+ const data = asRec(page.data?.data);
152
+
153
+ const refresh = () => {
154
+ void page.refetch();
155
+ void versions.refetch();
156
+ onSaved?.();
157
+ };
158
+ const toggle = useG8Mutation((kind: "publish" | "unpublish") => g8Post(`${base}/${kind}`), {
159
+ onSuccess: () => {
160
+ setConfirm(null);
161
+ refresh();
162
+ },
163
+ });
164
+ const restore = useG8Mutation((n: number) => g8Post(`${base}/versions/${n}/restore`), { onSuccess: refresh });
165
+ const preview = useG8Mutation(() => g8Get(`${base}/preview`), {
166
+ onSuccess: (res) => {
167
+ const url = text(asRec(res.data).url) || text(asRec(res.data).preview_url);
168
+ if (url && typeof window !== "undefined") window.open(url, "_blank", "noopener,noreferrer");
169
+ },
170
+ });
171
+
172
+ if (page.loading && !page.data) {
173
+ return (
174
+ <div className={className} aria-busy="true">
175
+ <Skeleton className="h-40 w-full" />
176
+ </div>
177
+ );
178
+ }
179
+ if (page.error) return <ApiError error={page.error} onRetry={() => void page.refetch()} />;
180
+
181
+ const sections = Array.isArray(data.sections) ? (data.sections as LandingSection[]) : null;
182
+ const published = text(data.status) === "published";
183
+ const url = liveUrl(data);
184
+ const history = rowsOf<Rec>(versions.data?.data, "versions");
185
+ const name = text(data.name, "this page");
186
+
187
+ return (
188
+ <div className={className}>
189
+ <div className="mb-4 flex flex-wrap items-center gap-2">
190
+ <h2 className="text-lg font-semibold">{text(data.name, "Untitled page")}</h2>
191
+ <span className="text-muted-foreground text-sm">
192
+ {text(data.status, "draft")}
193
+ {url ? ` · ${url}` : ""}
194
+ </span>
195
+ <div className="ml-auto flex gap-2">
196
+ <Button variant="outline" loading={preview.loading} onClick={() => void preview.mutate(undefined)}>
197
+ Preview
198
+ </Button>
199
+ <Button onClick={() => setConfirm(published ? "unpublish" : "publish")}>{published ? "Unpublish" : "Publish"}</Button>
200
+ </div>
201
+ </div>
202
+ {preview.error ? <ApiError error={preview.error} title="Could not open the preview" /> : null}
203
+ {toggle.error ? <ApiError error={toggle.error} title="Could not change the publish state" /> : null}
204
+
205
+ <DetailsCard key={`${pageId}-${text(data.id)}`} pageId={pageId} data={data} onSaved={refresh} />
206
+
207
+ {sections === null ? (
208
+ <p className="text-muted-foreground mb-4 text-sm">This is a freeform page, so it has no sections to edit here. Use Preview to review it.</p>
209
+ ) : (
210
+ <div className="mb-4 space-y-3">
211
+ {sections.map((s, i) => (
212
+ <SectionCard key={`${pageId}-${i}-${text(s.type)}`} index={i} section={s} pageId={pageId} onSaved={refresh} />
213
+ ))}
214
+ </div>
215
+ )}
216
+
217
+ <Card>
218
+ <CardHeader>
219
+ <CardTitle className="text-sm">Versions</CardTitle>
220
+ </CardHeader>
221
+ <CardContent className="space-y-2">
222
+ {history.length ? <p className="text-muted-foreground text-xs">Restore replaces the current content. The current state is saved as a version first, so it can be restored again.</p> : null}
223
+ {restore.error ? <ApiError error={restore.error} title="Could not restore that version" /> : null}
224
+ {history.length === 0 ? <p className="text-muted-foreground text-sm">No saved versions yet. Edits made with AI snapshot the page first.</p> : null}
225
+ {history.map((v) => (
226
+ <div key={text(v.id)} className="flex items-center gap-3 text-sm">
227
+ <span className="font-medium">v{text(v.version_num)}</span>
228
+ <span className="text-muted-foreground flex-1 truncate">
229
+ {text(v.edit_summary) || text(v.edit_instruction) || "Saved version"} · {shortDate(v.created_at)}
230
+ </span>
231
+ <Button size="sm" variant="outline" disabled={restore.loading} title="Replaces the current content; the current state is saved as a version first" onClick={() => void restore.mutate(Number(v.version_num))}>
232
+ Restore
233
+ </Button>
234
+ </div>
235
+ ))}
236
+ </CardContent>
237
+ </Card>
238
+
239
+ {confirm ? (
240
+ <ConfirmSpendDialog
241
+ open
242
+ tier="external"
243
+ title={confirm === "publish" ? `Publish "${name}"` : `Unpublish "${name}"`}
244
+ description={confirm === "publish" ? "The page goes live on a public URL." : "Every URL this page served stops resolving."}
245
+ confirmLabel={confirm === "publish" ? "Publish" : "Unpublish"}
246
+ confirming={toggle.loading}
247
+ onConfirm={() => void toggle.mutate(confirm)}
248
+ onCancel={() => setConfirm(null)}
249
+ />
250
+ ) : null}
251
+ </div>
252
+ );
253
+ }
@@ -0,0 +1,159 @@
1
+ "use client";
2
+
3
+ /**
4
+ * PageList: every landing page in the workspace, with status filter, create, publish, unpublish, delete.
5
+ *
6
+ * SDK operations, in the order the screen uses them:
7
+ * 1. listLandingPages GET /landing-pages?status&limit&offset (read)
8
+ * 2. createLandingPage POST /landing-pages (billable when `generate` is true)
9
+ * 3. publishLandingPage POST /landing-pages/{id}/publish (external: puts the page on a public URL)
10
+ * 4. unpublishLandingPage POST /landing-pages/{id}/unpublish (external: the URL stops resolving)
11
+ * 5. deleteLandingPage DELETE /landing-pages/{id} (destructive)
12
+ *
13
+ * Billable, external and destructive calls always open ConfirmSpendDialog first. "Blank page" sends
14
+ * `generate: false`, which is a plain write and needs no confirmation.
15
+ */
16
+
17
+ import { useEffect, useState } from "react";
18
+ import { Button, Input, Table, TableBody, TableCell, TableHead, TableHeader, TableRow, Skeleton } from "@graph8/sdk/ui";
19
+ import { g8Delete, g8Get, g8Post } from "@/g8/lib/g8-client";
20
+ import { useG8Query } from "@/g8/hooks/use-g8-query";
21
+ import { useG8Mutation } from "@/g8/hooks/use-g8-mutation";
22
+ import { ApiError } from "@/g8/components/api-error";
23
+ import { EmptyState } from "@/g8/components/empty-state";
24
+ import { ConfirmSpendDialog } from "@/g8/components/confirm-spend-dialog";
25
+ import type { ConfirmTier } from "@/g8/components/confirm-spend-dialog";
26
+ import { asRec, num, rowsOf, shortDate, text } from "../lib/studio";
27
+ import type { LandingPageRow } from "../lib/studio";
28
+
29
+ export interface PageListProps {
30
+ /** Called with the page id when a row is opened. Wire it to your PageEditor route. */
31
+ onOpen?: (pageId: string) => void;
32
+ pageSize?: number;
33
+ className?: string;
34
+ }
35
+
36
+ type Pending =
37
+ | { kind: "create"; name: string }
38
+ | { kind: "publish" | "unpublish" | "delete"; page: LandingPageRow };
39
+
40
+ const FILTERS: Array<{ value: string; label: string }> = [
41
+ { value: "", label: "All" },
42
+ { value: "draft", label: "Drafts" },
43
+ { value: "published", label: "Published" },
44
+ ];
45
+
46
+ function confirmCopy(p: Pending): { tier: ConfirmTier; title: string; description: string; label: string; recipients?: number; affected?: number } {
47
+ if (p.kind === "create") {
48
+ return { tier: "billable", title: `Generate "${p.name || "Untitled page"}" with AI`, description: "Writes the copy and layout for the whole page and spends credits.", label: "Generate" };
49
+ }
50
+ if (p.kind === "publish") {
51
+ return { tier: "external", title: `Publish "${p.page.name}"`, description: "The page goes live on a public URL and anyone with the link can see it.", label: "Publish" };
52
+ }
53
+ if (p.kind === "unpublish") {
54
+ return { tier: "external", title: `Unpublish "${p.page.name}"`, description: "Every URL this page served stops resolving. Links in ads and emails will 404.", label: "Unpublish" };
55
+ }
56
+ return { tier: "destructive", title: `Delete "${p.page.name}"`, description: "The page and its version history are removed.", label: "Delete", affected: 1 };
57
+ }
58
+
59
+ export function PageList({ onOpen, pageSize = 25, className }: PageListProps) {
60
+ const [status, setStatus] = useState("");
61
+ const [offset, setOffset] = useState(0);
62
+ const [name, setName] = useState("");
63
+ const [pending, setPending] = useState<Pending | null>(null);
64
+
65
+ const list = useG8Query(
66
+ (signal) => g8Get("/landing-pages", { status: status || undefined, limit: pageSize, offset }, { signal }),
67
+ [status, offset, pageSize],
68
+ );
69
+ const run = useG8Mutation(async (p: Pending) => {
70
+ if (p.kind === "create") return g8Post("/landing-pages", { name: p.name || undefined, generate: true });
71
+ const base = `/landing-pages/${encodeURIComponent(p.page.id)}`;
72
+ if (p.kind === "publish") return g8Post(`${base}/publish`);
73
+ if (p.kind === "unpublish") return g8Post(`${base}/unpublish`);
74
+ return g8Delete(base);
75
+ }, { onSuccess: () => { setPending(null); void list.refetch(); } });
76
+ const blank = useG8Mutation((title: string) => g8Post("/landing-pages", { name: title || undefined, generate: false }), {
77
+ onSuccess: (res) => {
78
+ setName("");
79
+ const id = text(asRec(res.data).id);
80
+ if (id) onOpen?.(id);
81
+ else void list.refetch();
82
+ },
83
+ });
84
+
85
+ const data = asRec(list.data?.data);
86
+ const pages = rowsOf<LandingPageRow>(data, "landing_pages");
87
+ const total = num(data.total) ?? pages.length;
88
+ // Deleting the last row of the last page leaves the offset past the end; step back to the previous page.
89
+ useEffect(() => { if (!list.loading && !list.error && pages.length === 0 && offset > 0) setOffset(Math.max(0, offset - pageSize)); }, [list.loading, list.error, pages.length, offset, pageSize]);
90
+ const copy = pending ? confirmCopy(pending) : null;
91
+
92
+ return (
93
+ <div className={className}>
94
+ <div className="mb-4 flex flex-wrap items-center gap-2">
95
+ <div className="flex gap-1" role="tablist" aria-label="Status">
96
+ {FILTERS.map((f) => (
97
+ <Button key={f.value} size="sm" variant={status === f.value ? "default" : "outline"} onClick={() => { setStatus(f.value); setOffset(0); }}>
98
+ {f.label}
99
+ </Button>
100
+ ))}
101
+ </div>
102
+ <div className="ml-auto flex items-center gap-2">
103
+ <Input aria-label="New page name" placeholder="Page name" value={name} onChange={(e) => setName(e.target.value)} className="w-56" />
104
+ <Button variant="outline" loading={blank.loading} onClick={() => blank.mutate(name)}>Blank page</Button>
105
+ <Button onClick={() => setPending({ kind: "create", name })}>Generate with AI</Button>
106
+ </div>
107
+ </div>
108
+
109
+ {list.error ? <ApiError error={list.error} onRetry={() => void list.refetch()} /> : null}
110
+ {run.error ? <ApiError error={run.error} title="That action failed" /> : null}
111
+ {blank.error ? <ApiError error={blank.error} title="Could not create the page" /> : null}
112
+
113
+ {list.loading && !list.data ? (
114
+ <div className="space-y-2" aria-busy="true">{[0, 1, 2].map((i) => <Skeleton key={i} className="h-10 w-full" />)}</div>
115
+ ) : !list.error && pages.length === 0 ? (
116
+ <EmptyState title="No landing pages yet" description="Generate a page with AI or start from a blank one, then edit its sections." />
117
+ ) : (
118
+ <Table>
119
+ <TableHeader>
120
+ <TableRow><TableHead>Name</TableHead><TableHead>Status</TableHead><TableHead>Sections</TableHead><TableHead>Updated</TableHead><TableHead className="text-right">Actions</TableHead></TableRow>
121
+ </TableHeader>
122
+ <TableBody>
123
+ {pages.map((page) => {
124
+ const live = page.status === "published";
125
+ return (
126
+ <TableRow key={page.id}>
127
+ <TableCell>
128
+ <button type="button" className="font-medium hover:underline" onClick={() => onOpen?.(page.id)}>{page.name || "Untitled page"}</button>
129
+ {page.published_url ? <div className="text-muted-foreground text-xs">{page.published_url}</div> : null}
130
+ </TableCell>
131
+ <TableCell>{page.status}</TableCell>
132
+ <TableCell>{page.generation_mode === "freeform" ? "Freeform" : page.section_count ?? 0}</TableCell>
133
+ <TableCell>{shortDate(page.updated_at)}</TableCell>
134
+ <TableCell className="space-x-1 text-right">
135
+ <Button size="sm" variant="outline" onClick={() => setPending({ kind: live ? "unpublish" : "publish", page })}>{live ? "Unpublish" : "Publish"}</Button>
136
+ <Button size="sm" variant="ghost" aria-label={`Delete ${page.name}`} onClick={() => setPending({ kind: "delete", page })}>Delete</Button>
137
+ </TableCell>
138
+ </TableRow>
139
+ );
140
+ })}
141
+ </TableBody>
142
+ </Table>
143
+ )}
144
+
145
+ <div className="mt-3 flex items-center justify-between text-sm">
146
+ <span className="text-muted-foreground">{total} {total === 1 ? "page" : "pages"}</span>
147
+ <div className="flex gap-2">
148
+ <Button size="sm" variant="outline" disabled={offset === 0} onClick={() => setOffset(Math.max(0, offset - pageSize))}>Previous</Button>
149
+ <Button size="sm" variant="outline" disabled={offset + pageSize >= total} onClick={() => setOffset(offset + pageSize)}>Next</Button>
150
+ </div>
151
+ </div>
152
+
153
+ {pending && copy ? (
154
+ <ConfirmSpendDialog open tier={copy.tier} title={copy.title} description={copy.description} confirmLabel={copy.label}
155
+ affected={copy.affected} confirming={run.loading} onConfirm={() => void run.mutate(pending)} onCancel={() => setPending(null)} />
156
+ ) : null}
157
+ </div>
158
+ );
159
+ }