@pylonsync/create-pylon 0.19.1 → 0.20.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 (675) hide show
  1. package/bin/create-pylon.js +30 -7
  2. package/package.json +1 -1
  3. package/templates/agency/.env.example +7 -0
  4. package/templates/agency/README.md +28 -3
  5. package/templates/agency/app/(marketing)/contact-form.tsx +20 -18
  6. package/templates/agency/app/(marketing)/page.tsx +3 -3
  7. package/templates/agency/app/(marketing)/work/page.tsx +3 -3
  8. package/templates/agency/app/auth-form.tsx +4 -4
  9. package/templates/agency/app/dashboard/dashboard-client.tsx +110 -64
  10. package/templates/agency/app/dashboard/page.tsx +14 -16
  11. package/templates/agency/app/icon.svg +3 -0
  12. package/templates/agency/app/login/page.tsx +12 -14
  13. package/templates/agency/components/marketing.tsx +12 -5
  14. package/templates/agency/functions/seedStudioBackoffice.ts +42 -11
  15. package/templates/agency/lib/agency.ts +22 -0
  16. package/templates/agency/lib/demo.ts +118 -0
  17. package/templates/agency/lib/site.config.ts +19 -13
  18. package/templates/agency/public/favicon.ico +0 -0
  19. package/templates/agency/tests/agency.test.ts +57 -0
  20. package/templates/ai-chat/.env.example +13 -27
  21. package/templates/ai-chat/README.md +53 -60
  22. package/templates/ai-chat/app/(chat)/c/[id]/page.tsx +19 -0
  23. package/templates/ai-chat/app/(chat)/chat-client.tsx +657 -622
  24. package/templates/ai-chat/app/(chat)/layout.tsx +6 -43
  25. package/templates/ai-chat/app/(chat)/not-found.tsx +6 -10
  26. package/templates/ai-chat/app/(chat)/page.tsx +7 -11
  27. package/templates/ai-chat/app/auth-form.tsx +45 -38
  28. package/templates/ai-chat/app/error.tsx +9 -12
  29. package/templates/ai-chat/app/globals.css +464 -102
  30. package/templates/ai-chat/app/layout.tsx +11 -19
  31. package/templates/ai-chat/app/login/page.tsx +39 -24
  32. package/templates/ai-chat/app.ts +118 -43
  33. package/templates/ai-chat/components/chat/composer.tsx +193 -0
  34. package/templates/ai-chat/components/chat/empty-state.tsx +65 -0
  35. package/templates/ai-chat/components/chat/icons.tsx +198 -0
  36. package/templates/ai-chat/components/chat/markdown.tsx +164 -0
  37. package/templates/ai-chat/components/chat/message.tsx +183 -0
  38. package/templates/ai-chat/components/chat/setup.tsx +71 -0
  39. package/templates/ai-chat/components/chat/sidebar.tsx +288 -0
  40. package/templates/ai-chat/functions/_beginTurn.ts +145 -0
  41. package/templates/ai-chat/functions/_finishTurn.ts +55 -0
  42. package/templates/ai-chat/functions/assistantStatus.ts +13 -0
  43. package/templates/ai-chat/functions/claimGuestHistory.ts +39 -0
  44. package/templates/ai-chat/functions/createConversation.ts +28 -0
  45. package/templates/ai-chat/functions/deleteConversation.ts +15 -20
  46. package/templates/ai-chat/functions/prepareGuestClaim.ts +32 -0
  47. package/templates/ai-chat/functions/renameConversation.ts +19 -0
  48. package/templates/ai-chat/functions/respond.ts +88 -0
  49. package/templates/ai-chat/functions/seedExamples.ts +46 -0
  50. package/templates/ai-chat/functions/stopResponse.ts +30 -0
  51. package/templates/ai-chat/lib/brand-icon.ts +7 -0
  52. package/templates/ai-chat/lib/chat.ts +277 -0
  53. package/templates/ai-chat/lib/claim.ts +15 -0
  54. package/templates/ai-chat/lib/examples.ts +179 -0
  55. package/templates/ai-chat/lib/highlight.ts +107 -0
  56. package/templates/ai-chat/lib/markdown.ts +317 -0
  57. package/templates/ai-chat/lib/provider.ts +38 -0
  58. package/templates/ai-chat/lib/site.config.ts +55 -39
  59. package/templates/ai-chat/tests/chat.test.ts +174 -0
  60. package/templates/ai-chat/tests/components.test.tsx +57 -0
  61. package/templates/ai-chat/tests/markdown.test.ts +88 -0
  62. package/templates/ai-chat/tests/provider.test.ts +32 -0
  63. package/templates/backend/consumer/apps/api/app/layout.tsx +33 -0
  64. package/templates/backend/consumer/apps/api/app/page.tsx +54 -0
  65. package/templates/backend/consumer/apps/api/app.ts +166 -0
  66. package/templates/backend/consumer/apps/api/functions/addComment.ts +24 -0
  67. package/templates/backend/consumer/apps/api/functions/createPost.ts +19 -36
  68. package/templates/backend/consumer/apps/api/functions/deletePost.ts +15 -13
  69. package/templates/backend/consumer/apps/api/functions/myProfile.ts +5 -8
  70. package/templates/backend/consumer/apps/api/functions/seedDemo.ts +95 -0
  71. package/templates/backend/consumer/apps/api/functions/setFollow.ts +36 -0
  72. package/templates/backend/consumer/apps/api/functions/setLike.ts +36 -0
  73. package/templates/backend/consumer/apps/api/functions/upsertProfile.ts +43 -37
  74. package/templates/backend/consumer/apps/api/lib/seed.ts +89 -0
  75. package/templates/backend/consumer/apps/api/lib/social.ts +50 -0
  76. package/templates/backend/consumer/apps/api/package.json +8 -2
  77. package/templates/backend/consumer/apps/api/public/images/posts/arch.jpg +0 -0
  78. package/templates/backend/consumer/apps/api/public/images/posts/coffee.jpg +0 -0
  79. package/templates/backend/consumer/apps/api/public/images/posts/dog.jpg +0 -0
  80. package/templates/backend/consumer/apps/api/public/images/posts/lisbon.jpg +0 -0
  81. package/templates/backend/consumer/apps/api/public/images/posts/ramen.jpg +0 -0
  82. package/templates/backend/consumer/apps/api/public/images/posts/run.jpg +0 -0
  83. package/templates/backend/consumer/apps/api/public/images/posts/sunset.jpg +0 -0
  84. package/templates/backend/consumer/apps/api/tsconfig.json +12 -2
  85. package/templates/chat/README.md +43 -21
  86. package/templates/chat/app/chat-client.tsx +495 -85
  87. package/templates/chat/app/error.tsx +1 -1
  88. package/templates/chat/app/globals.css +170 -54
  89. package/templates/chat/app/layout.tsx +12 -9
  90. package/templates/chat/app/not-found.tsx +4 -2
  91. package/templates/chat/app/page.tsx +9 -19
  92. package/templates/chat/app.ts +96 -17
  93. package/templates/chat/components/chat/avatar.tsx +45 -0
  94. package/templates/chat/components/chat/composer.tsx +110 -0
  95. package/templates/chat/components/chat/message-list.tsx +285 -0
  96. package/templates/chat/components/chat/sidebar.tsx +190 -0
  97. package/templates/chat/functions/joinChat.ts +83 -0
  98. package/templates/chat/functions/renameMember.ts +25 -0
  99. package/templates/chat/lib/chat.ts +188 -0
  100. package/templates/chat/lib/seed.ts +78 -0
  101. package/templates/chat/tests/chat.test.ts +159 -0
  102. package/templates/chat/tests/composer.test.tsx +54 -0
  103. package/templates/consumer/README.md +3 -3
  104. package/templates/consumer/app/globals.css +10 -3
  105. package/templates/consumer/app/icon.svg +1 -0
  106. package/templates/consumer/components/social/avatar.tsx +25 -11
  107. package/templates/consumer/functions/seedFeed.ts +3 -3
  108. package/templates/consumer/lib/seed.ts +43 -85
  109. package/templates/consumer/public/favicon.ico +0 -0
  110. package/templates/consumer/public/images/posts/arch.jpg +0 -0
  111. package/templates/consumer/public/images/posts/coffee.jpg +0 -0
  112. package/templates/consumer/public/images/posts/dog.jpg +0 -0
  113. package/templates/consumer/public/images/posts/lisbon.jpg +0 -0
  114. package/templates/consumer/public/images/posts/ramen.jpg +0 -0
  115. package/templates/consumer/public/images/posts/run.jpg +0 -0
  116. package/templates/consumer/public/images/posts/sunset.jpg +0 -0
  117. package/templates/consumer/tests/seed.test.ts +39 -0
  118. package/templates/creator/.env.example +7 -0
  119. package/templates/creator/README.md +24 -5
  120. package/templates/creator/app/(marketing)/newsletter-signup.tsx +33 -12
  121. package/templates/creator/app/auth-form.tsx +7 -7
  122. package/templates/creator/app/dashboard/dashboard-client.tsx +137 -100
  123. package/templates/creator/app/dashboard/page.tsx +18 -18
  124. package/templates/creator/app/icon.svg +2 -0
  125. package/templates/creator/app/login/page.tsx +14 -14
  126. package/templates/creator/app.ts +4 -4
  127. package/templates/creator/functions/seedDemo.ts +32 -0
  128. package/templates/creator/functions/subscribe.ts +4 -15
  129. package/templates/creator/lib/demo.ts +94 -0
  130. package/templates/creator/lib/site.config.ts +15 -10
  131. package/templates/creator/lib/stats.ts +35 -0
  132. package/templates/creator/public/favicon.ico +0 -0
  133. package/templates/creator/tests/counter.test.ts +92 -0
  134. package/templates/creator/tests/live-count.test.tsx +45 -0
  135. package/templates/crm/README.md +22 -1
  136. package/templates/crm/app/apple-icon.png +0 -0
  137. package/templates/crm/app/companies/companies-view.tsx +32 -14
  138. package/templates/crm/app/companies/page.tsx +2 -1
  139. package/templates/crm/app/contacts/contacts-view.tsx +34 -10
  140. package/templates/crm/app/contacts/page.tsx +2 -1
  141. package/templates/crm/app/deals/[id]/deal-view.tsx +137 -98
  142. package/templates/crm/app/deals/[id]/page.tsx +2 -1
  143. package/templates/crm/app/globals.css +29 -14
  144. package/templates/crm/app/icon.svg +1 -0
  145. package/templates/crm/app/layout.tsx +7 -5
  146. package/templates/crm/app/page.tsx +2 -1
  147. package/templates/crm/app/pipeline-view.tsx +5 -5
  148. package/templates/crm/app/workspace.tsx +29 -17
  149. package/templates/crm/app.ts +5 -5
  150. package/templates/crm/components/activity-timeline.tsx +14 -4
  151. package/templates/crm/components/app-shell.tsx +94 -0
  152. package/templates/crm/components/auth-form.tsx +35 -5
  153. package/templates/crm/components/avatar.tsx +5 -1
  154. package/templates/crm/components/board-skeleton.tsx +5 -5
  155. package/templates/crm/components/brand-mark.tsx +25 -0
  156. package/templates/crm/components/command-palette.tsx +17 -17
  157. package/templates/crm/components/data-table.tsx +97 -36
  158. package/templates/crm/components/deal-card.tsx +23 -27
  159. package/templates/crm/components/filter-tabs.tsx +101 -0
  160. package/templates/crm/components/metrics-bar.tsx +6 -6
  161. package/templates/crm/components/nav.tsx +15 -0
  162. package/templates/crm/components/page-header.tsx +43 -7
  163. package/templates/crm/components/pipeline-board.tsx +16 -10
  164. package/templates/crm/components/require-auth.tsx +24 -26
  165. package/templates/crm/components/sidebar.tsx +54 -49
  166. package/templates/crm/components/stage-badge.tsx +4 -2
  167. package/templates/crm/components/ui/button.tsx +5 -4
  168. package/templates/crm/components/ui/dialog.tsx +1 -1
  169. package/templates/crm/components/ui/input.tsx +1 -1
  170. package/templates/crm/components/ui/select.tsx +1 -1
  171. package/templates/crm/components/ui/textarea.tsx +1 -1
  172. package/templates/crm/functions/seedWorkspace.ts +32 -17
  173. package/templates/crm/lib/brand.ts +9 -0
  174. package/templates/crm/lib/demo-team.ts +51 -0
  175. package/templates/crm/lib/pipeline.ts +37 -0
  176. package/templates/crm/lib/search.ts +4 -3
  177. package/templates/crm/lib/seed.ts +53 -8
  178. package/templates/crm/public/favicon.ico +0 -0
  179. package/templates/crm/tests/components.test.tsx +5 -5
  180. package/templates/crm/tests/demo-team.test.ts +18 -0
  181. package/templates/crm/tests/pipeline.test.ts +59 -3
  182. package/templates/directory/.env.example +7 -0
  183. package/templates/directory/README.md +22 -3
  184. package/templates/directory/app/(marketing)/directory-browse.tsx +30 -12
  185. package/templates/directory/app/(marketing)/page.tsx +6 -4
  186. package/templates/directory/app/auth-form.tsx +4 -4
  187. package/templates/directory/app/dashboard/dashboard-client.tsx +177 -90
  188. package/templates/directory/app/dashboard/page.tsx +16 -14
  189. package/templates/directory/app/icon.svg +4 -0
  190. package/templates/directory/app/login/page.tsx +16 -15
  191. package/templates/directory/functions/seedDemo.ts +43 -0
  192. package/templates/directory/lib/demo.ts +145 -0
  193. package/templates/directory/lib/directory.ts +27 -0
  194. package/templates/directory/lib/site.config.ts +21 -16
  195. package/templates/directory/public/favicon.ico +0 -0
  196. package/templates/directory/tests/directory.test.ts +46 -0
  197. package/templates/expo/consumer/apps/expo/App.tsx +360 -288
  198. package/templates/expo/consumer/apps/expo/app.json +9 -1
  199. package/templates/expo/consumer/apps/expo/package.json +2 -1
  200. package/templates/helpdesk/README.md +21 -0
  201. package/templates/helpdesk/app/apple-icon.png +0 -0
  202. package/templates/helpdesk/app/customers/customers-view.tsx +23 -7
  203. package/templates/helpdesk/app/customers/page.tsx +2 -1
  204. package/templates/helpdesk/app/globals.css +29 -14
  205. package/templates/helpdesk/app/icon.svg +1 -0
  206. package/templates/helpdesk/app/inbox-view.tsx +66 -47
  207. package/templates/helpdesk/app/layout.tsx +7 -5
  208. package/templates/helpdesk/app/page.tsx +4 -7
  209. package/templates/helpdesk/app/tickets/[id]/page.tsx +2 -1
  210. package/templates/helpdesk/app/tickets/[id]/ticket-view.tsx +134 -97
  211. package/templates/helpdesk/app/workspace.tsx +33 -22
  212. package/templates/helpdesk/app.ts +2 -2
  213. package/templates/helpdesk/components/app-shell.tsx +94 -0
  214. package/templates/helpdesk/components/auth-form.tsx +35 -5
  215. package/templates/helpdesk/components/avatar.tsx +5 -1
  216. package/templates/helpdesk/components/brand-mark.tsx +28 -0
  217. package/templates/helpdesk/components/command-palette.tsx +17 -17
  218. package/templates/helpdesk/components/data-table.tsx +97 -36
  219. package/templates/helpdesk/components/filter-tabs.tsx +101 -0
  220. package/templates/helpdesk/components/nav.tsx +14 -0
  221. package/templates/helpdesk/components/page-header.tsx +43 -7
  222. package/templates/helpdesk/components/priority-badge.tsx +49 -6
  223. package/templates/helpdesk/components/require-auth.tsx +24 -26
  224. package/templates/helpdesk/components/sidebar.tsx +54 -48
  225. package/templates/helpdesk/components/sla-indicator.tsx +5 -8
  226. package/templates/helpdesk/components/thread.tsx +7 -7
  227. package/templates/helpdesk/components/ticket-list.tsx +79 -19
  228. package/templates/helpdesk/components/ui/button.tsx +5 -4
  229. package/templates/helpdesk/components/ui/dialog.tsx +1 -1
  230. package/templates/helpdesk/components/ui/input.tsx +1 -1
  231. package/templates/helpdesk/components/ui/select.tsx +1 -1
  232. package/templates/helpdesk/components/ui/textarea.tsx +1 -1
  233. package/templates/helpdesk/functions/seedWorkspace.ts +25 -15
  234. package/templates/helpdesk/lib/brand.ts +9 -0
  235. package/templates/helpdesk/lib/demo-team.ts +51 -0
  236. package/templates/helpdesk/lib/search.ts +4 -3
  237. package/templates/helpdesk/lib/seed.ts +43 -2
  238. package/templates/helpdesk/lib/tickets.ts +68 -20
  239. package/templates/helpdesk/public/favicon.ico +0 -0
  240. package/templates/helpdesk/tests/components.test.tsx +10 -0
  241. package/templates/helpdesk/tests/demo-team.test.ts +18 -0
  242. package/templates/helpdesk/tests/tickets.test.ts +70 -0
  243. package/templates/imessage/.env.example +49 -0
  244. package/templates/imessage/AGENTS.md +153 -0
  245. package/templates/imessage/CLAUDE.md +7 -0
  246. package/templates/imessage/README.md +187 -0
  247. package/templates/imessage/app/console.tsx +382 -0
  248. package/templates/imessage/app/contacts/page.tsx +13 -0
  249. package/templates/imessage/app/error.tsx +16 -0
  250. package/templates/imessage/app/gate.tsx +31 -0
  251. package/templates/imessage/app/globals.css +183 -0
  252. package/templates/imessage/app/layout.tsx +17 -0
  253. package/templates/imessage/app/login/login-form.tsx +115 -0
  254. package/templates/imessage/app/login/page.tsx +30 -0
  255. package/templates/imessage/app/not-found.tsx +13 -0
  256. package/templates/imessage/app/page.tsx +14 -0
  257. package/templates/imessage/app/robots.ts +6 -0
  258. package/templates/imessage/app/setup/page.tsx +13 -0
  259. package/templates/imessage/app/sign-out.tsx +20 -0
  260. package/templates/imessage/app.ts +299 -0
  261. package/templates/imessage/bunfig +4 -0
  262. package/templates/imessage/components/avatar.tsx +46 -0
  263. package/templates/imessage/components/contact-panel.tsx +263 -0
  264. package/templates/imessage/components/contacts-view.tsx +172 -0
  265. package/templates/imessage/components/conversation-list.tsx +129 -0
  266. package/templates/imessage/components/nav-rail.tsx +67 -0
  267. package/templates/imessage/components/setup-view.tsx +408 -0
  268. package/templates/imessage/components/thread.tsx +229 -0
  269. package/templates/imessage/components/ui/badge.tsx +37 -0
  270. package/templates/imessage/components/ui/button.tsx +56 -0
  271. package/templates/imessage/components/ui/card.tsx +90 -0
  272. package/templates/imessage/components/ui/dialog.tsx +110 -0
  273. package/templates/imessage/components/ui/input.tsx +30 -0
  274. package/templates/imessage/components/ui/label.tsx +21 -0
  275. package/templates/imessage/components/ui/select.tsx +39 -0
  276. package/templates/imessage/components/ui/table.tsx +124 -0
  277. package/templates/imessage/components/ui/textarea.tsx +24 -0
  278. package/templates/imessage/components.json +20 -0
  279. package/templates/imessage/functions/answerNow.ts +34 -0
  280. package/templates/imessage/functions/assistant.ts +96 -0
  281. package/templates/imessage/functions/beginTurn.ts +101 -0
  282. package/templates/imessage/functions/cancelReminder.ts +13 -0
  283. package/templates/imessage/functions/claimOutbound.ts +44 -0
  284. package/templates/imessage/functions/clearDemo.ts +18 -0
  285. package/templates/imessage/functions/deleteNote.ts +11 -0
  286. package/templates/imessage/functions/deliverOutbound.ts +55 -0
  287. package/templates/imessage/functions/finishTurn.ts +84 -0
  288. package/templates/imessage/functions/fireReminder.ts +42 -0
  289. package/templates/imessage/functions/ingestInbound.ts +22 -0
  290. package/templates/imessage/functions/ownerAccount.ts +13 -0
  291. package/templates/imessage/functions/processTurn.ts +74 -0
  292. package/templates/imessage/functions/relayApply.ts +102 -0
  293. package/templates/imessage/functions/relaySync.ts +63 -0
  294. package/templates/imessage/functions/releaseOutbound.ts +18 -0
  295. package/templates/imessage/functions/resolveTurn.ts +25 -0
  296. package/templates/imessage/functions/saveContact.ts +27 -0
  297. package/templates/imessage/functions/seedDemo.ts +70 -0
  298. package/templates/imessage/functions/sendOwnerMessage.ts +28 -0
  299. package/templates/imessage/functions/sendblueWebhook.ts +49 -0
  300. package/templates/imessage/functions/setContactStatus.ts +17 -0
  301. package/templates/imessage/functions/settleOutbound.ts +35 -0
  302. package/templates/imessage/functions/setupStatus.ts +33 -0
  303. package/templates/imessage/functions/sweep.ts +58 -0
  304. package/templates/imessage/functions/toolCancelReminder.ts +17 -0
  305. package/templates/imessage/functions/toolForgetNote.ts +15 -0
  306. package/templates/imessage/functions/toolListNotes.ts +16 -0
  307. package/templates/imessage/functions/toolListReminders.ts +17 -0
  308. package/templates/imessage/functions/toolSaveNote.ts +22 -0
  309. package/templates/imessage/functions/toolSetReminder.ts +55 -0
  310. package/templates/imessage/functions/updateSettings.ts +48 -0
  311. package/templates/imessage/gitignore +11 -0
  312. package/templates/imessage/lib/agent-call.ts +251 -0
  313. package/templates/imessage/lib/assistant.ts +74 -0
  314. package/templates/imessage/lib/chunk.ts +135 -0
  315. package/templates/imessage/lib/demo.ts +68 -0
  316. package/templates/imessage/lib/format.ts +170 -0
  317. package/templates/imessage/lib/gate.ts +104 -0
  318. package/templates/imessage/lib/handles.ts +67 -0
  319. package/templates/imessage/lib/provider.ts +38 -0
  320. package/templates/imessage/lib/relay-protocol.ts +95 -0
  321. package/templates/imessage/lib/reminders.ts +41 -0
  322. package/templates/imessage/lib/secrets.ts +59 -0
  323. package/templates/imessage/lib/sendblue.ts +173 -0
  324. package/templates/imessage/lib/store.ts +407 -0
  325. package/templates/imessage/lib/transport.ts +135 -0
  326. package/templates/imessage/lib/utils.ts +10 -0
  327. package/templates/imessage/package.json +40 -0
  328. package/templates/imessage/relay/.env.example +14 -0
  329. package/templates/imessage/relay/applescript.ts +70 -0
  330. package/templates/imessage/relay/chatdb.ts +165 -0
  331. package/templates/imessage/relay/client.ts +41 -0
  332. package/templates/imessage/relay/main.ts +137 -0
  333. package/templates/imessage/relay/state.ts +35 -0
  334. package/templates/imessage/tests/agent-call.test.ts +112 -0
  335. package/templates/imessage/tests/applescript.test.ts +88 -0
  336. package/templates/imessage/tests/chatdb.test.ts +139 -0
  337. package/templates/imessage/tests/chunk.test.ts +61 -0
  338. package/templates/imessage/tests/fake-ctx.ts +88 -0
  339. package/templates/imessage/tests/ingest.test.ts +141 -0
  340. package/templates/imessage/tests/lib.test.ts +99 -0
  341. package/templates/imessage/tests/review-fixes.test.ts +138 -0
  342. package/templates/imessage/tests/secrets.test.ts +36 -0
  343. package/templates/imessage/tests/sendblue.test.ts +143 -0
  344. package/templates/imessage/tests/setup.ts +5 -0
  345. package/templates/imessage/tsconfig.json +18 -0
  346. package/templates/inventory/README.md +21 -0
  347. package/templates/inventory/app/apple-icon.png +0 -0
  348. package/templates/inventory/app/globals.css +29 -14
  349. package/templates/inventory/app/icon.svg +1 -0
  350. package/templates/inventory/app/layout.tsx +7 -5
  351. package/templates/inventory/app/movements/movements-view.tsx +32 -10
  352. package/templates/inventory/app/movements/page.tsx +2 -1
  353. package/templates/inventory/app/page.tsx +4 -6
  354. package/templates/inventory/app/products/[id]/page.tsx +2 -1
  355. package/templates/inventory/app/products/[id]/product-view.tsx +55 -25
  356. package/templates/inventory/app/products-view.tsx +74 -33
  357. package/templates/inventory/app/workspace.tsx +34 -16
  358. package/templates/inventory/app.ts +10 -2
  359. package/templates/inventory/components/app-shell.tsx +94 -0
  360. package/templates/inventory/components/auth-form.tsx +35 -5
  361. package/templates/inventory/components/avatar.tsx +5 -1
  362. package/templates/inventory/components/brand-mark.tsx +25 -0
  363. package/templates/inventory/components/category-tile.tsx +49 -0
  364. package/templates/inventory/components/command-palette.tsx +17 -17
  365. package/templates/inventory/components/data-table.tsx +97 -36
  366. package/templates/inventory/components/filter-tabs.tsx +101 -0
  367. package/templates/inventory/components/nav.tsx +14 -0
  368. package/templates/inventory/components/page-header.tsx +43 -7
  369. package/templates/inventory/components/require-auth.tsx +24 -26
  370. package/templates/inventory/components/sidebar.tsx +54 -48
  371. package/templates/inventory/components/stock-level.tsx +30 -14
  372. package/templates/inventory/components/summary-bar.tsx +8 -15
  373. package/templates/inventory/components/ui/button.tsx +5 -4
  374. package/templates/inventory/components/ui/dialog.tsx +1 -1
  375. package/templates/inventory/components/ui/input.tsx +1 -1
  376. package/templates/inventory/components/ui/select.tsx +1 -1
  377. package/templates/inventory/components/ui/textarea.tsx +1 -1
  378. package/templates/inventory/functions/seedWorkspace.ts +18 -8
  379. package/templates/inventory/lib/brand.ts +9 -0
  380. package/templates/inventory/lib/demo-team.ts +51 -0
  381. package/templates/inventory/lib/search.ts +4 -3
  382. package/templates/inventory/lib/seed.ts +29 -2
  383. package/templates/inventory/lib/stock.ts +32 -0
  384. package/templates/inventory/public/favicon.ico +0 -0
  385. package/templates/inventory/tests/demo-team.test.ts +18 -0
  386. package/templates/inventory/tests/stock.test.ts +29 -0
  387. package/templates/invoices/README.md +16 -0
  388. package/templates/invoices/app/apple-icon.png +0 -0
  389. package/templates/invoices/app/clients/clients-view.tsx +18 -6
  390. package/templates/invoices/app/clients/page.tsx +2 -1
  391. package/templates/invoices/app/globals.css +29 -14
  392. package/templates/invoices/app/icon.svg +1 -0
  393. package/templates/invoices/app/invoices/[id]/invoice-view.tsx +72 -48
  394. package/templates/invoices/app/invoices/[id]/page.tsx +2 -1
  395. package/templates/invoices/app/invoices-view.tsx +81 -32
  396. package/templates/invoices/app/layout.tsx +7 -5
  397. package/templates/invoices/app/page.tsx +4 -6
  398. package/templates/invoices/app/workspace.tsx +26 -16
  399. package/templates/invoices/app.ts +2 -2
  400. package/templates/invoices/components/app-shell.tsx +94 -0
  401. package/templates/invoices/components/auth-form.tsx +35 -5
  402. package/templates/invoices/components/avatar.tsx +5 -1
  403. package/templates/invoices/components/brand-mark.tsx +26 -0
  404. package/templates/invoices/components/command-palette.tsx +17 -17
  405. package/templates/invoices/components/data-table.tsx +97 -36
  406. package/templates/invoices/components/filter-tabs.tsx +101 -0
  407. package/templates/invoices/components/line-items.tsx +19 -14
  408. package/templates/invoices/components/nav.tsx +14 -0
  409. package/templates/invoices/components/page-header.tsx +43 -7
  410. package/templates/invoices/components/require-auth.tsx +24 -26
  411. package/templates/invoices/components/sidebar.tsx +54 -48
  412. package/templates/invoices/components/status-badge.tsx +7 -7
  413. package/templates/invoices/components/summary-bar.tsx +5 -5
  414. package/templates/invoices/components/ui/button.tsx +5 -4
  415. package/templates/invoices/components/ui/dialog.tsx +1 -1
  416. package/templates/invoices/components/ui/input.tsx +1 -1
  417. package/templates/invoices/components/ui/select.tsx +1 -1
  418. package/templates/invoices/components/ui/textarea.tsx +1 -1
  419. package/templates/invoices/functions/recordPayment.ts +3 -0
  420. package/templates/invoices/lib/billing.ts +40 -0
  421. package/templates/invoices/lib/brand.ts +9 -0
  422. package/templates/invoices/lib/search.ts +4 -3
  423. package/templates/invoices/public/favicon.ico +0 -0
  424. package/templates/invoices/tests/billing.test.ts +27 -0
  425. package/templates/ios/consumer/apps/ios/Package.swift +4 -3
  426. package/templates/ios/consumer/apps/ios/Sources/__APP_NAME_PASCAL__/ActivityView.swift +107 -0
  427. package/templates/ios/consumer/apps/ios/Sources/__APP_NAME_PASCAL__/AppModel.swift +171 -0
  428. package/templates/ios/consumer/apps/ios/Sources/__APP_NAME_PASCAL__/AuthView.swift +182 -0
  429. package/templates/ios/consumer/apps/ios/Sources/__APP_NAME_PASCAL__/CommentsView.swift +136 -0
  430. package/templates/ios/consumer/apps/ios/Sources/__APP_NAME_PASCAL__/ComposeView.swift +152 -0
  431. package/templates/ios/consumer/apps/ios/Sources/__APP_NAME_PASCAL__/ExploreView.swift +117 -0
  432. package/templates/ios/consumer/apps/ios/Sources/__APP_NAME_PASCAL__/FeedView.swift +175 -159
  433. package/templates/ios/consumer/apps/ios/Sources/__APP_NAME_PASCAL__/MainTabView.swift +54 -0
  434. package/templates/ios/consumer/apps/ios/Sources/__APP_NAME_PASCAL__/Media.swift +271 -0
  435. package/templates/ios/consumer/apps/ios/Sources/__APP_NAME_PASCAL__/Models.swift +101 -4
  436. package/templates/ios/consumer/apps/ios/Sources/__APP_NAME_PASCAL__/PostCard.swift +245 -0
  437. package/templates/ios/consumer/apps/ios/Sources/__APP_NAME_PASCAL__/ProfileEditorView.swift +208 -0
  438. package/templates/ios/consumer/apps/ios/Sources/__APP_NAME_PASCAL__/ProfileView.swift +183 -0
  439. package/templates/ios/consumer/apps/ios/Sources/__APP_NAME_PASCAL__/RootView.swift +25 -31
  440. package/templates/ios/consumer/apps/ios/Sources/__APP_NAME_PASCAL__/SocialStore.swift +285 -0
  441. package/templates/ios/consumer/apps/ios/Sources/__APP_NAME_PASCAL__/StoryViewer.swift +155 -0
  442. package/templates/ios/consumer/apps/ios/Sources/__APP_NAME_PASCAL__/Theme.swift +82 -0
  443. package/templates/ios/consumer/apps/ios/Sources/__APP_NAME_PASCAL__/__APP_NAME_PASCAL__App.swift +4 -47
  444. package/templates/ios/consumer/apps/ios/project.yml +13 -9
  445. package/templates/local-service/.env.example +7 -0
  446. package/templates/local-service/README.md +26 -2
  447. package/templates/local-service/app/(marketing)/booking-widget.tsx +48 -9
  448. package/templates/local-service/app/(marketing)/page.tsx +66 -42
  449. package/templates/local-service/app/auth-form.tsx +8 -8
  450. package/templates/local-service/app/dashboard/dashboard-client.tsx +119 -92
  451. package/templates/local-service/app/dashboard/page.tsx +14 -14
  452. package/templates/local-service/app/icon.svg +2 -0
  453. package/templates/local-service/app/login/page.tsx +15 -15
  454. package/templates/local-service/functions/seedDemo.ts +49 -0
  455. package/templates/local-service/lib/demo.ts +135 -0
  456. package/templates/local-service/lib/hours.ts +44 -0
  457. package/templates/local-service/lib/site.config.ts +8 -3
  458. package/templates/local-service/lib/slots.ts +3 -0
  459. package/templates/local-service/public/favicon.ico +0 -0
  460. package/templates/local-service/tests/booking.test.ts +104 -0
  461. package/templates/marketplace/README.md +23 -15
  462. package/templates/marketplace/app/globals.css +172 -114
  463. package/templates/marketplace/app/layout.tsx +75 -30
  464. package/templates/marketplace/app/listing/[id]/page.tsx +210 -123
  465. package/templates/marketplace/app/me/page.tsx +4 -4
  466. package/templates/marketplace/app/page.tsx +221 -298
  467. package/templates/marketplace/app/sell/page.tsx +8 -11
  468. package/templates/marketplace/app.ts +32 -7
  469. package/templates/marketplace/client/ActivityFeed.tsx +311 -0
  470. package/templates/marketplace/client/AuthNav.tsx +24 -16
  471. package/templates/marketplace/client/ListingCard.tsx +118 -0
  472. package/templates/marketplace/client/LiveGrid.tsx +92 -0
  473. package/templates/marketplace/client/LiveListingStatus.tsx +32 -24
  474. package/templates/marketplace/client/LoginCard.tsx +114 -136
  475. package/templates/marketplace/client/MarketProvider.tsx +3 -5
  476. package/templates/marketplace/client/MyMarket.tsx +371 -185
  477. package/templates/marketplace/client/OfferPanel.tsx +421 -287
  478. package/templates/marketplace/client/SellForm.tsx +271 -192
  479. package/templates/marketplace/client/ThemeToggle.tsx +1 -0
  480. package/templates/marketplace/client/WatchButton.tsx +16 -16
  481. package/templates/marketplace/client/market.ts +61 -14
  482. package/templates/marketplace/functions/seedMarket.ts +143 -36
  483. package/templates/marketplace/functions/seedOffers.ts +101 -0
  484. package/templates/marketplace/lib/catalog.ts +72 -1
  485. package/templates/marketplace/public/images/listings/ceramic-lamp-640.webp +0 -0
  486. package/templates/marketplace/public/images/listings/ceramic-lamp.webp +0 -0
  487. package/templates/marketplace/public/images/listings/duffel-640.webp +0 -0
  488. package/templates/marketplace/public/images/listings/duffel.webp +0 -0
  489. package/templates/marketplace/public/images/listings/film-camera-640.webp +0 -0
  490. package/templates/marketplace/public/images/listings/film-camera.webp +0 -0
  491. package/templates/marketplace/public/images/listings/lounge-chair-640.webp +0 -0
  492. package/templates/marketplace/public/images/listings/lounge-chair.webp +0 -0
  493. package/templates/marketplace/public/images/listings/road-bike-640.webp +0 -0
  494. package/templates/marketplace/public/images/listings/road-bike.webp +0 -0
  495. package/templates/marketplace/public/images/listings/slipper-chair-640.webp +0 -0
  496. package/templates/marketplace/public/images/listings/slipper-chair.webp +0 -0
  497. package/templates/marketplace/public/images/listings/turntable-640.webp +0 -0
  498. package/templates/marketplace/public/images/listings/turntable.webp +0 -0
  499. package/templates/marketplace/public/images/listings/wardrobe-640.webp +0 -0
  500. package/templates/marketplace/public/images/listings/wardrobe.webp +0 -0
  501. package/templates/marketplace/public/images/listings/watch-640.webp +0 -0
  502. package/templates/marketplace/public/images/listings/watch.webp +0 -0
  503. package/templates/marketplace/public/images/listings/weekender-bag-640.webp +0 -0
  504. package/templates/marketplace/public/images/listings/weekender-bag.webp +0 -0
  505. package/templates/marketplace/tests/example.test.ts +64 -2
  506. package/templates/marketplace/tests/listing-card.test.tsx +61 -0
  507. package/templates/projects/README.md +24 -3
  508. package/templates/projects/app/apple-icon.png +0 -0
  509. package/templates/projects/app/clients/clients-view.tsx +20 -6
  510. package/templates/projects/app/clients/page.tsx +2 -1
  511. package/templates/projects/app/globals.css +29 -14
  512. package/templates/projects/app/icon.svg +1 -0
  513. package/templates/projects/app/layout.tsx +7 -5
  514. package/templates/projects/app/page.tsx +3 -5
  515. package/templates/projects/app/projects/[id]/page.tsx +2 -1
  516. package/templates/projects/app/projects/[id]/project-view.tsx +45 -30
  517. package/templates/projects/app/projects-view.tsx +105 -52
  518. package/templates/projects/app/workspace.tsx +41 -20
  519. package/templates/projects/app.ts +2 -2
  520. package/templates/projects/components/app-shell.tsx +94 -0
  521. package/templates/projects/components/auth-form.tsx +35 -5
  522. package/templates/projects/components/avatar.tsx +5 -1
  523. package/templates/projects/components/board-skeleton.tsx +3 -3
  524. package/templates/projects/components/brand-mark.tsx +25 -0
  525. package/templates/projects/components/command-palette.tsx +17 -17
  526. package/templates/projects/components/data-table.tsx +97 -36
  527. package/templates/projects/components/filter-tabs.tsx +101 -0
  528. package/templates/projects/components/meter.tsx +48 -0
  529. package/templates/projects/components/nav.tsx +14 -0
  530. package/templates/projects/components/page-header.tsx +43 -7
  531. package/templates/projects/components/require-auth.tsx +24 -26
  532. package/templates/projects/components/sidebar.tsx +54 -48
  533. package/templates/projects/components/summary-bar.tsx +49 -0
  534. package/templates/projects/components/task-board.tsx +10 -9
  535. package/templates/projects/components/time-dialog.tsx +29 -5
  536. package/templates/projects/components/ui/button.tsx +5 -4
  537. package/templates/projects/components/ui/dialog.tsx +1 -1
  538. package/templates/projects/components/ui/input.tsx +1 -1
  539. package/templates/projects/components/ui/select.tsx +1 -1
  540. package/templates/projects/components/ui/textarea.tsx +1 -1
  541. package/templates/projects/functions/seedWorkspace.ts +26 -12
  542. package/templates/projects/lib/brand.ts +9 -0
  543. package/templates/projects/lib/demo-team.ts +51 -0
  544. package/templates/projects/lib/search.ts +4 -3
  545. package/templates/projects/lib/seed.ts +35 -1
  546. package/templates/projects/lib/work.ts +63 -0
  547. package/templates/projects/public/favicon.ico +0 -0
  548. package/templates/projects/tests/demo-team.test.ts +18 -0
  549. package/templates/projects/tests/work.test.ts +65 -0
  550. package/templates/restaurant/.env.example +7 -0
  551. package/templates/restaurant/README.md +29 -3
  552. package/templates/restaurant/app/(marketing)/layout.tsx +4 -2
  553. package/templates/restaurant/app/(marketing)/page.tsx +47 -23
  554. package/templates/restaurant/app/(marketing)/reservation-widget.tsx +37 -7
  555. package/templates/restaurant/app/auth-form.tsx +8 -8
  556. package/templates/restaurant/app/dashboard/dashboard-client.tsx +241 -109
  557. package/templates/restaurant/app/dashboard/page.tsx +14 -14
  558. package/templates/restaurant/app/icon.svg +4 -0
  559. package/templates/restaurant/app/login/page.tsx +15 -15
  560. package/templates/restaurant/functions/seedDemo.ts +44 -0
  561. package/templates/restaurant/lib/demo.ts +144 -0
  562. package/templates/restaurant/lib/hours.ts +45 -0
  563. package/templates/restaurant/lib/site.config.ts +3 -3
  564. package/templates/restaurant/public/favicon.ico +0 -0
  565. package/templates/restaurant/tests/reservations.test.ts +67 -0
  566. package/templates/saas/README.md +54 -35
  567. package/templates/saas/app/(auth)/auth-form.tsx +25 -23
  568. package/templates/saas/app/(auth)/forgot-password/page.tsx +1 -1
  569. package/templates/saas/app/(auth)/layout.tsx +27 -33
  570. package/templates/saas/app/(auth)/login/page.tsx +1 -1
  571. package/templates/saas/app/(auth)/reset-password/page.tsx +1 -1
  572. package/templates/saas/app/(auth)/signup/page.tsx +1 -1
  573. package/templates/saas/app/(marketing)/layout.tsx +127 -253
  574. package/templates/saas/app/(marketing)/page.tsx +133 -372
  575. package/templates/saas/app/(marketing)/pricing/page.tsx +21 -19
  576. package/templates/saas/app/(marketing)/products/[slug]/page.tsx +40 -95
  577. package/templates/saas/app/dashboard/board-client.tsx +416 -0
  578. package/templates/saas/app/dashboard/dashboard-client.tsx +30 -615
  579. package/templates/saas/app/dashboard/layout.tsx +27 -13
  580. package/templates/saas/app/dashboard/overview-client.tsx +332 -0
  581. package/templates/saas/app/dashboard/page.tsx +29 -35
  582. package/templates/saas/app/dashboard/projects/[id]/page.tsx +29 -0
  583. package/templates/saas/app/dashboard/projects/page.tsx +11 -7
  584. package/templates/saas/app/dashboard/projects-client.tsx +398 -0
  585. package/templates/saas/app/globals.css +5 -4
  586. package/templates/saas/app/icon.svg +1 -0
  587. package/templates/saas/app/onboarding/onboarding-client.tsx +93 -26
  588. package/templates/saas/app/onboarding/page.tsx +6 -2
  589. package/templates/saas/app.ts +68 -13
  590. package/templates/saas/components/dashboard-shell.tsx +212 -132
  591. package/templates/saas/components/marketing.tsx +137 -274
  592. package/templates/saas/components/pricing-table.tsx +12 -11
  593. package/templates/saas/components/setup-checklist.tsx +31 -29
  594. package/templates/saas/components/task-ui.tsx +199 -0
  595. package/templates/saas/functions/createProject.ts +7 -13
  596. package/templates/saas/functions/createTask.ts +67 -0
  597. package/templates/saas/functions/deleteProject.ts +17 -0
  598. package/templates/saas/functions/deleteTask.ts +14 -0
  599. package/templates/saas/functions/seedWorkspace.ts +46 -0
  600. package/templates/saas/functions/setProjectStatus.ts +41 -0
  601. package/templates/saas/functions/updateProfile.ts +13 -0
  602. package/templates/saas/functions/updateProject.ts +4 -5
  603. package/templates/saas/functions/updateTask.ts +88 -0
  604. package/templates/saas/lib/members.ts +53 -0
  605. package/templates/saas/lib/names.ts +53 -0
  606. package/templates/saas/lib/plans.ts +11 -0
  607. package/templates/saas/lib/sample-workspace.ts +117 -0
  608. package/templates/saas/lib/site.config.ts +200 -463
  609. package/templates/saas/lib/tasks.ts +101 -0
  610. package/templates/saas/lib/use-live.ts +25 -0
  611. package/templates/saas/public/favicon.ico +0 -0
  612. package/templates/saas/public/screenshots/board.png +0 -0
  613. package/templates/saas/public/screenshots/members.png +0 -0
  614. package/templates/saas/public/screenshots/overview.png +0 -0
  615. package/templates/saas/public/screenshots/projects.png +0 -0
  616. package/templates/saas/scripts/capture-screenshots.mjs +156 -0
  617. package/templates/saas/tests/names.test.ts +35 -0
  618. package/templates/saas/tests/plans.test.ts +12 -0
  619. package/templates/saas/tests/sample-workspace.test.ts +46 -0
  620. package/templates/saas/tests/tasks.test.ts +81 -0
  621. package/templates/shop/README.md +25 -13
  622. package/templates/shop/app/(marketing)/layout.tsx +94 -67
  623. package/templates/shop/app/(marketing)/not-found.tsx +13 -12
  624. package/templates/shop/app/(marketing)/page.tsx +120 -59
  625. package/templates/shop/app/(marketing)/shop-client.tsx +364 -237
  626. package/templates/shop/app/(marketing)/success/page.tsx +75 -22
  627. package/templates/shop/app/globals.css +94 -1
  628. package/templates/shop/app/layout.tsx +4 -4
  629. package/templates/shop/app/login/page.tsx +4 -4
  630. package/templates/shop/app.ts +18 -6
  631. package/templates/shop/components/cart-button.tsx +33 -0
  632. package/templates/shop/components/marketing.tsx +17 -87
  633. package/templates/shop/components/shelf-count.tsx +36 -0
  634. package/templates/shop/functions/checkout.ts +17 -1
  635. package/templates/shop/lib/cart-store.ts +57 -0
  636. package/templates/shop/lib/shelf-store.ts +35 -0
  637. package/templates/shop/lib/shop.ts +27 -0
  638. package/templates/shop/lib/site.config.ts +108 -98
  639. package/templates/shop/public/images/hero.jpg +0 -0
  640. package/templates/shop/public/images/products/cedar-smoke.jpg +0 -0
  641. package/templates/shop/public/images/products/fig-leaf.jpg +0 -0
  642. package/templates/shop/public/images/products/linen.jpg +0 -0
  643. package/templates/shop/public/images/products/sea-salt.jpg +0 -0
  644. package/templates/shop/public/images/products/tobacco-oak.jpg +0 -0
  645. package/templates/shop/tests/shop.test.ts +64 -0
  646. package/templates/waitlist/.env.example +7 -0
  647. package/templates/waitlist/README.md +29 -13
  648. package/templates/waitlist/app/(marketing)/page.tsx +9 -7
  649. package/templates/waitlist/app/(marketing)/waitlist-hero.tsx +64 -31
  650. package/templates/waitlist/app/auth-form.tsx +8 -8
  651. package/templates/waitlist/app/dashboard/dashboard-client.tsx +132 -95
  652. package/templates/waitlist/app/dashboard/page.tsx +17 -16
  653. package/templates/waitlist/app/icon.svg +2 -0
  654. package/templates/waitlist/app/login/page.tsx +14 -14
  655. package/templates/waitlist/app.ts +6 -6
  656. package/templates/waitlist/functions/joinWaitlist.ts +6 -18
  657. package/templates/waitlist/functions/seedDemo.ts +32 -0
  658. package/templates/waitlist/lib/demo.ts +94 -0
  659. package/templates/waitlist/lib/launch.ts +19 -0
  660. package/templates/waitlist/lib/site.config.ts +14 -9
  661. package/templates/waitlist/lib/stats.ts +35 -0
  662. package/templates/waitlist/public/favicon.ico +0 -0
  663. package/templates/waitlist/tests/counter.test.ts +107 -0
  664. package/templates/waitlist/tests/live-count.test.tsx +48 -0
  665. package/templates/ai-chat/tests/example.test.ts +0 -12
  666. package/templates/backend/consumer/apps/api/functions/feed.ts +0 -57
  667. package/templates/backend/consumer/apps/api/functions/profilePosts.ts +0 -17
  668. package/templates/backend/consumer/apps/api/functions/toggleLike.ts +0 -48
  669. package/templates/backend/consumer/apps/api/schema.ts +0 -143
  670. package/templates/chat/functions/_keep.ts +0 -13
  671. package/templates/chat/tests/example.test.ts +0 -12
  672. package/templates/ios/consumer/apps/ios/Sources/__APP_NAME_PASCAL__/ProfileSetupView.swift +0 -68
  673. package/templates/marketplace/client/LiveTicker.tsx +0 -116
  674. package/templates/marketplace/client/ScrollToListingsLink.tsx +0 -29
  675. package/templates/marketplace/public/images/marketplace-hero.webp +0 -0
@@ -3,14 +3,14 @@
3
3
  import React, { useEffect, useMemo, useState } from "react";
4
4
  import { db, callFn } from "@pylonsync/react";
5
5
  import { useAuth } from "@pylonsync/client";
6
+ import { Download, LogOut, Search } from "lucide-react";
6
7
  import type { SubscriberStatsData, SubscriberStatsResult, SubscriberRow } from "@/lib/stats";
7
8
 
8
- // The owner's live dashboard. Liveness rides the SAME public aggregate the
9
+ // The owner's live dashboard. Liveness rides the same public aggregate the
9
10
  // landing page uses: `db.useQuery("SubscriberCount")` re-renders the instant the
10
- // count changes (cross-tab, via the replica). The emails themselves never sync
11
- // — they come from the owner-gated `subscriberStats` function, (re)fetched on
12
- // mount and whenever the live count ticks. So the total, chart, and list all
13
- // stay live, but PII only ever travels through the gated call.
11
+ // count changes (cross-tab, via the replica). The emails themselves never sync.
12
+ // They come from the owner-gated `subscriberStats` function, fetched on mount and
13
+ // again whenever the live count changes, so PII only travels through that call.
14
14
  export function SubscriberDashboard({ userEmail }: { userEmail: string }) {
15
15
  const { data: statRows } = db.useQuery<{ id: string; count: number }>("SubscriberCount");
16
16
  const liveCount = statRows.length > 0 ? statRows[0].count : 0;
@@ -19,6 +19,12 @@ export function SubscriberDashboard({ userEmail }: { userEmail: string }) {
19
19
  const [denied, setDenied] = useState(false);
20
20
  const [error, setError] = useState<string | null>(null);
21
21
 
22
+ // In `pylon dev` an empty list gets demo subscribers (functions/seedDemo.ts).
23
+ // On a deploy the call returns without writing.
24
+ useEffect(() => {
25
+ void callFn("seedDemo", {}).catch(() => {});
26
+ }, []);
27
+
22
28
  useEffect(() => {
23
29
  let cancelled = false;
24
30
  callFn<SubscriberStatsResult>("subscriberStats", {})
@@ -46,7 +52,7 @@ export function SubscriberDashboard({ userEmail }: { userEmail: string }) {
46
52
  if (denied) return <OwnerOnly email={userEmail} />;
47
53
  if (error) {
48
54
  return (
49
- <div className="rounded-xl border border-red-200 bg-red-50 px-5 py-4 text-sm text-red-700">
55
+ <div className="rounded-lg border border-red-200 bg-red-50 px-5 py-4 text-sm text-red-700">
50
56
  {error}
51
57
  </div>
52
58
  );
@@ -54,16 +60,27 @@ export function SubscriberDashboard({ userEmail }: { userEmail: string }) {
54
60
  if (!data) return <Skeleton />;
55
61
 
56
62
  return (
57
- <div className="space-y-8">
58
- <div>
59
- <h1 className="text-xl font-semibold tracking-tight">Subscribers</h1>
60
- <p className="mt-1 text-sm text-zinc-500">
61
- Live — new subscribers appear here the moment they sign up.
63
+ <div className="space-y-10">
64
+ <div className="flex flex-col gap-2 sm:flex-row sm:items-end sm:justify-between">
65
+ <div>
66
+ <h1 className="text-[2.25rem] font-medium leading-none text-ink">
67
+ Subscribers
68
+ </h1>
69
+ <p className="mt-3 text-[16px] text-ink-2">
70
+ New subscribers appear here as they sign up. Your site shows the same total.
71
+ </p>
72
+ </div>
73
+ <p className="flex items-center gap-2 text-[13px] text-ink-2">
74
+ <span className="relative flex size-2">
75
+ <span className="absolute inline-flex size-2 animate-ping rounded-full bg-brand/60" />
76
+ <span className="relative inline-flex size-2 rounded-full bg-brand" />
77
+ </span>
78
+ live
62
79
  </p>
63
80
  </div>
64
81
 
65
- <div className="grid gap-4 sm:grid-cols-3">
66
- <Stat label="Total subscribers" value={data.total} />
82
+ <div className="grid gap-px overflow-hidden rounded-lg border border-rule bg-rule grid-cols-3">
83
+ <Stat label="Subscribers" value={data.total} />
67
84
  <Stat label="Last 7 days" value={data.last7} />
68
85
  <Stat label="Today" value={data.today} />
69
86
  </div>
@@ -77,12 +94,10 @@ export function SubscriberDashboard({ userEmail }: { userEmail: string }) {
77
94
 
78
95
  function Stat({ label, value }: { label: string; value: number }) {
79
96
  return (
80
- <div className="rounded-xl border border-zinc-200 bg-white p-4">
81
- <div className="text-[11px] font-medium uppercase tracking-wide text-zinc-400">
82
- {label}
83
- </div>
84
- <div className="mt-1 text-2xl font-semibold tabular-nums text-zinc-900">
85
- {value.toLocaleString()}
97
+ <div className="bg-white px-3 py-4 sm:px-5 sm:py-5">
98
+ <div className="text-[13px] text-ink-2">{label}</div>
99
+ <div className="mt-2 text-[1.75rem] sm:text-[2.25rem] font-medium leading-none tabular-nums text-ink">
100
+ {value.toLocaleString("en-US")}
86
101
  </div>
87
102
  </div>
88
103
  );
@@ -92,121 +107,145 @@ function Stat({ label, value }: { label: string; value: number }) {
92
107
 
93
108
  function TrendChart({ daily }: { daily: { date: string; count: number }[] }) {
94
109
  const max = Math.max(1, ...daily.map((d) => d.count));
110
+ const total = daily.reduce((n, d) => n + d.count, 0);
95
111
  return (
96
- <div className="rounded-xl border border-zinc-200 bg-white p-5">
97
- <div className="flex items-center justify-between">
98
- <h2 className="text-sm font-semibold text-zinc-900">Last 30 days</h2>
99
- <span className="text-[12px] text-zinc-400">
100
- peak {max.toLocaleString()}/day
112
+ <section className="rounded-lg border border-rule bg-white p-5 sm:p-6">
113
+ <div className="flex items-baseline justify-between gap-4">
114
+ <h2 className="text-[19px] font-medium text-ink">Last 30 days</h2>
115
+ <span className="text-[13px] text-ink-2">
116
+ {total.toLocaleString("en-US")} new · peak {max}/day
101
117
  </span>
102
118
  </div>
103
- {/* Each column is a FULL-HEIGHT flex cell (h-full) that bottom-aligns its
104
- bar — so the bar's percentage height resolves against the 7rem track,
105
- not an auto-height parent (which would collapse it to nothing). */}
106
- <div className="mt-4 flex h-28 items-end gap-1">
107
- {daily.map((d) => (
108
- <div
109
- key={d.date}
110
- className="group relative flex h-full flex-1 flex-col justify-end"
111
- >
119
+ {/* Each column is a full-height flex cell that bottom-aligns its bar, so
120
+ the bar's percentage height resolves against the track. */}
121
+ <div className="mt-6 flex h-36 items-end gap-[3px]">
122
+ {daily.map((d, i) => (
123
+ <div key={d.date} className="group relative flex h-full flex-1 flex-col justify-end">
112
124
  <div
113
- className="w-full rounded-t bg-brand/80 transition-colors group-hover:bg-brand"
125
+ className={
126
+ "w-full rounded-t-[2px] transition-colors " +
127
+ (i === daily.length - 1 ? "bg-brand" : "bg-brand/45 group-hover:bg-brand/80")
128
+ }
114
129
  style={{ height: `${Math.max(2, (d.count / max) * 100)}%` }}
115
130
  />
116
- {/* Tooltip on hover — date + count. */}
117
- <div className="pointer-events-none absolute bottom-full left-1/2 z-10 mb-1 hidden -translate-x-1/2 whitespace-nowrap rounded bg-zinc-900 px-2 py-1 text-[11px] text-white group-hover:block">
131
+ <div className="pointer-events-none absolute bottom-full left-1/2 z-10 mb-1.5 hidden -translate-x-1/2 whitespace-nowrap rounded border border-rule bg-ink px-2 py-1 text-[12px] text-paper group-hover:block">
118
132
  {fmtDay(d.date)} · {d.count}
119
133
  </div>
120
134
  </div>
121
135
  ))}
122
136
  </div>
123
- <div className="mt-2 flex justify-between text-[11px] text-zinc-400">
137
+ <div className="mt-3 flex justify-between border-t border-rule pt-2 text-[11px] text-ink-2">
124
138
  <span>{fmtDay(daily[0]?.date)}</span>
125
- <span>{fmtDay(daily[daily.length - 1]?.date)}</span>
139
+ <span>Today</span>
126
140
  </div>
127
- </div>
141
+ </section>
128
142
  );
129
143
  }
130
144
 
131
- /* ----------------------------- subscriber list ---------------------------- */
145
+ /* --------------------------- subscriber list -------------------------- */
146
+
147
+ const PAGE = 50;
132
148
 
133
149
  function SubscriberTable({ subscribers }: { subscribers: SubscriberRow[] }) {
134
150
  const [q, setQ] = useState("");
151
+ const [shown, setShown] = useState(PAGE);
135
152
  const filtered = useMemo(() => {
136
153
  const needle = q.trim().toLowerCase();
137
154
  if (!needle) return subscribers;
138
155
  return subscribers.filter((s) => s.email.toLowerCase().includes(needle));
139
156
  }, [q, subscribers]);
157
+ // Join order: the oldest subscriber is No. 1.
158
+ const position = useMemo(() => {
159
+ const m = new Map<string, number>();
160
+ subscribers.forEach((s, i) => m.set(s.id, subscribers.length - i));
161
+ return m;
162
+ }, [subscribers]);
140
163
 
141
164
  return (
142
- <div className="rounded-xl border border-zinc-200 bg-white">
143
- <div className="flex flex-col gap-3 border-b border-zinc-100 p-4 sm:flex-row sm:items-center sm:justify-between">
144
- <h2 className="text-sm font-semibold text-zinc-900">
145
- Subscribers{" "}
146
- <span className="font-normal text-zinc-400">
147
- ({filtered.length.toLocaleString()})
165
+ <section className="overflow-hidden rounded-lg border border-rule bg-white">
166
+ <div className="flex flex-col gap-3 border-b border-rule p-4 sm:flex-row sm:items-center sm:justify-between sm:px-5">
167
+ <h2 className="text-[19px] font-medium text-ink">
168
+ All subscribers{" "}
169
+ <span className="text-[13px] font-normal text-ink-2">
170
+ {filtered.length.toLocaleString("en-US")}
148
171
  </span>
149
172
  </h2>
150
173
  <div className="flex items-center gap-2">
151
- <input
152
- value={q}
153
- onChange={(e) => setQ(e.target.value)}
154
- placeholder="Search email…"
155
- aria-label="Search subscribers"
156
- className="h-9 w-44 rounded-md border border-zinc-300 bg-white px-3 text-sm outline-none transition placeholder:text-zinc-400 focus:border-brand focus:ring-2 focus:ring-brand/20"
157
- />
174
+ <label className="relative flex-1 sm:flex-none">
175
+ <Search aria-hidden className="pointer-events-none absolute left-2.5 top-1/2 size-3.5 -translate-y-1/2 text-ink-2" />
176
+ <input
177
+ value={q}
178
+ onChange={(e) => setQ(e.target.value)}
179
+ placeholder="Search email"
180
+ aria-label="Search subscribers"
181
+ className="h-9 w-full rounded-md border border-rule bg-white pl-8 pr-3 text-[13px] text-ink outline-none transition placeholder:text-ink-2/60 focus:border-brand sm:w-52"
182
+ />
183
+ </label>
158
184
  <button
159
185
  type="button"
160
186
  onClick={() => exportCsv(subscribers)}
161
187
  disabled={subscribers.length === 0}
162
- className="inline-flex h-9 items-center rounded-md border border-zinc-300 px-3 text-[13px] font-medium text-zinc-700 transition-colors hover:bg-zinc-50 disabled:opacity-40"
188
+ className="inline-flex h-9 shrink-0 items-center gap-1.5 rounded-md border border-rule px-3 text-[13px] font-medium text-ink transition-colors hover:border-ink-2 disabled:opacity-40"
163
189
  >
190
+ <Download aria-hidden className="size-3.5" />
164
191
  Export CSV
165
192
  </button>
166
193
  </div>
167
194
  </div>
168
195
 
169
196
  {filtered.length === 0 ? (
170
- <p className="p-8 text-center text-sm text-zinc-500">
197
+ <p className="px-5 py-12 text-center text-[16px] text-ink-2">
171
198
  {subscribers.length === 0
172
- ? "No subscribers yet. Share your landing page and watch them roll in — live."
199
+ ? "No subscribers yet. They appear here as soon as someone signs up."
173
200
  : "No emails match your search."}
174
201
  </p>
175
202
  ) : (
176
- <ul className="divide-y divide-zinc-100">
177
- {filtered.map((s) => (
178
- <li key={s.id} className="flex items-center justify-between gap-3 px-4 py-2.5">
179
- <span className="truncate text-sm text-zinc-800">{s.email}</span>
180
- <span className="shrink-0 text-[12px] text-zinc-400">
181
- {fmtDateTime(s.createdAt)}
182
- </span>
183
- </li>
184
- ))}
185
- </ul>
203
+ <>
204
+ <ul className="divide-y divide-rule">
205
+ {filtered.slice(0, shown).map((s) => (
206
+ <li key={s.id} className="flex items-center gap-4 px-4 py-3 sm:px-5">
207
+ <span className="hidden w-12 shrink-0 sm:block text-[12px] tabular-nums text-ink-2">
208
+ {position.get(s.id)}
209
+ </span>
210
+ <span className="min-w-0 flex-1 truncate text-[14px] text-ink">{s.email}</span>
211
+ <span className="shrink-0 text-[13px] text-ink-2">
212
+ {fmtDateTime(s.createdAt)}
213
+ </span>
214
+ </li>
215
+ ))}
216
+ </ul>
217
+ {filtered.length > shown ? (
218
+ <button
219
+ type="button"
220
+ onClick={() => setShown((n) => n + PAGE)}
221
+ className="w-full border-t border-rule py-3 text-[13px] text-ink-2 transition-colors hover:text-ink"
222
+ >
223
+ Show {Math.min(PAGE, filtered.length - shown)} more
224
+ </button>
225
+ ) : null}
226
+ </>
186
227
  )}
187
- </div>
228
+ </section>
188
229
  );
189
230
  }
190
231
 
191
232
  /* --------------------------- owner-only gate -------------------------- */
192
233
 
193
234
  // Shown to a signed-in user the `subscriberStats` function refused (not the
194
- // configured owner, or no PYLON_OWNER_EMAIL set). Fails closed — no subscriber data
235
+ // configured owner, or no PYLON_OWNER_EMAIL set). Fails closed: no subscriber data
195
236
  // is fetched or shown.
196
237
  function OwnerOnly({ email }: { email: string }) {
197
238
  return (
198
- <div className="rounded-xl border border-dashed border-zinc-300 px-6 py-12 text-center">
199
- <h1 className="text-lg font-semibold">This dashboard is owner-only</h1>
200
- <p className="mx-auto mt-2 max-w-md text-sm text-zinc-500">
201
- You&apos;re signed in as{" "}
202
- <span className="font-medium text-zinc-700">{email || "this account"}</span>.
203
- A newsletter is single-tenant — only the owner can see the subscribers. Set{" "}
204
- <code className="rounded bg-zinc-100 px-1.5 py-0.5 text-[12px]">
205
- PYLON_OWNER_EMAIL={email || "you@yourbusiness.com"}
239
+ <div className="rounded-lg border border-dashed border-rule px-6 py-14 text-center">
240
+ <h1 className="text-[1.5rem] font-medium text-ink">This dashboard is owner-only</h1>
241
+ <p className="mx-auto mt-3 max-w-md text-[14px] leading-relaxed text-ink-2">
242
+ You are signed in as <span className="text-ink">{email || "this account"}</span>. Only
243
+ the owner can see subscribers. Set{" "}
244
+ <code className="rounded bg-rule/50 px-1.5 py-0.5 text-[12px] text-ink">
245
+ PYLON_OWNER_EMAIL={email || "you@yoursite.com"}
206
246
  </code>{" "}
207
- in your{" "}
208
- <code className="rounded bg-zinc-100 px-1.5 py-0.5 text-[12px]">.env</code>,
209
- restart, and reload — or sign in with the owner account.
247
+ in <code className="rounded bg-rule/50 px-1.5 py-0.5 text-[12px] text-ink">.env</code>,
248
+ restart, and reload. Or sign in with the owner account.
210
249
  </p>
211
250
  </div>
212
251
  );
@@ -223,20 +262,22 @@ export function UserMenu({ email }: { email: string }) {
223
262
  }
224
263
  return (
225
264
  <details className="group relative">
226
- <summary className="flex size-8 cursor-pointer select-none list-none items-center justify-center rounded-full bg-zinc-900 text-[12px] font-semibold text-white marker:hidden [&::-webkit-details-marker]:hidden">
265
+ <summary
266
+ aria-label="Account"
267
+ className="flex size-8 cursor-pointer select-none list-none items-center justify-center rounded-full bg-brand text-[12px] font-semibold text-white marker:hidden [&::-webkit-details-marker]:hidden"
268
+ >
227
269
  {initial}
228
270
  </summary>
229
- <div className="absolute right-0 top-full z-40 mt-2 w-56 overflow-hidden rounded-xl border border-zinc-200 bg-white py-1 shadow-[0_16px_48px_-16px_rgba(0,0,0,0.25)]">
230
- <div className="border-b border-zinc-100 px-3 py-2">
231
- <div className="truncate text-[13px] font-medium text-zinc-900">
232
- {email || "Signed in"}
233
- </div>
271
+ <div className="absolute right-0 top-full z-40 mt-2 w-60 overflow-hidden rounded-lg border border-rule bg-white py-1 shadow-[0_24px_48px_-24px_rgba(42,38,34,0.35)]">
272
+ <div className="truncate border-b border-rule px-3 py-2.5 text-[13px] text-ink">
273
+ {email || "Signed in"}
234
274
  </div>
235
275
  <button
236
276
  type="button"
237
277
  onClick={onSignOut}
238
- className="flex w-full items-center px-3 py-2 text-left text-[13px] text-zinc-700 transition-colors hover:bg-zinc-50"
278
+ className="flex w-full items-center gap-2 px-3 py-2 text-left text-[13px] text-ink-2 transition-colors hover:bg-rule/50 hover:text-ink"
239
279
  >
280
+ <LogOut aria-hidden className="size-3.5" />
240
281
  Sign out
241
282
  </button>
242
283
  </div>
@@ -248,31 +289,27 @@ export function UserMenu({ email }: { email: string }) {
248
289
 
249
290
  function Skeleton() {
250
291
  return (
251
- <div className="space-y-8">
252
- <div className="h-6 w-32 animate-pulse rounded bg-zinc-100" />
292
+ <div className="space-y-10">
293
+ <div className="h-8 w-40 animate-pulse rounded bg-rule/50" />
253
294
  <div className="grid gap-4 sm:grid-cols-3">
254
295
  {[0, 1, 2].map((i) => (
255
- <div key={i} className="h-20 animate-pulse rounded-xl bg-zinc-100" />
296
+ <div key={i} className="h-24 animate-pulse rounded-lg bg-rule/50" />
256
297
  ))}
257
298
  </div>
258
- <div className="h-44 animate-pulse rounded-xl bg-zinc-100" />
259
- <div className="h-64 animate-pulse rounded-xl bg-zinc-100" />
299
+ <div className="h-56 animate-pulse rounded-lg bg-rule/50" />
300
+ <div className="h-72 animate-pulse rounded-lg bg-rule/50" />
260
301
  </div>
261
302
  );
262
303
  }
263
304
 
264
305
  function exportCsv(subscribers: SubscriberRow[]) {
265
- const cell = (v: string) =>
266
- /[",\n]/.test(v) ? `"${v.replace(/"/g, '""')}"` : v;
267
- const rows = [
268
- "email,joined_at",
269
- ...subscribers.map((s) => `${cell(s.email)},${cell(s.createdAt)}`),
270
- ];
306
+ const cell = (v: string) => (/[",\n]/.test(v) ? `"${v.replace(/"/g, '""')}"` : v);
307
+ const rows = ["email,subscribed_at", ...subscribers.map((s) => `${cell(s.email)},${cell(s.createdAt)}`)];
271
308
  const blob = new Blob([rows.join("\n")], { type: "text/csv;charset=utf-8" });
272
309
  const url = URL.createObjectURL(blob);
273
310
  const a = document.createElement("a");
274
311
  a.href = url;
275
- a.download = `newsletter-${new Date().toISOString().slice(0, 10)}.csv`;
312
+ a.download = `subscribers-${new Date().toISOString().slice(0, 10)}.csv`;
276
313
  document.body.appendChild(a);
277
314
  a.click();
278
315
  a.remove();
@@ -282,13 +319,13 @@ function exportCsv(subscribers: SubscriberRow[]) {
282
319
  function fmtDay(iso?: string) {
283
320
  if (!iso) return "";
284
321
  const d = new Date(iso + "T00:00:00Z");
285
- return d.toLocaleDateString(undefined, { month: "short", day: "numeric" });
322
+ return d.toLocaleDateString("en-US", { month: "short", day: "numeric", timeZone: "UTC" });
286
323
  }
287
324
 
288
325
  function fmtDateTime(iso: string) {
289
326
  const t = Date.parse(iso);
290
327
  if (Number.isNaN(t)) return "";
291
- return new Date(t).toLocaleString(undefined, {
328
+ return new Date(t).toLocaleString("en-US", {
292
329
  month: "short",
293
330
  day: "numeric",
294
331
  hour: "numeric",
@@ -1,5 +1,6 @@
1
1
  import React, { use } from "react";
2
2
  import { Link, type Metadata, type PageProps } from "@pylonsync/react";
3
+ import { ArrowUpRight } from "lucide-react";
3
4
  import { siteConfig } from "@/lib/site.config";
4
5
  import { UserMenu, SubscriberDashboard } from "./dashboard-client";
5
6
 
@@ -35,35 +36,34 @@ export default function DashboardPage({ auth, response, serverData }: PageProps)
35
36
  );
36
37
  }
37
38
 
38
- // Dashboard chrome: a slim top bar with the logo, a link back to the public
39
- // site, and the account menu (a client island for sign-out).
39
+ // Dashboard chrome in the site's own look: the cream page, the serif face,
40
+ // the headshot and name from the site header, a link back to the public site,
41
+ // and the account menu (a client island for sign-out).
40
42
  function Shell({ email, children }: { email: string; children: React.ReactNode }) {
41
- const { brand } = siteConfig;
43
+ const { brand, newsletter } = siteConfig;
42
44
  return (
43
- <div className="flex min-h-screen flex-col bg-white text-zinc-900">
44
- <header className="border-b border-zinc-200">
45
- <div className="mx-auto flex h-14 max-w-4xl items-center justify-between px-6">
46
- <div className="flex items-center gap-2">
47
- <span className="flex size-6 items-center justify-center rounded-[7px] bg-zinc-900 text-[13px] font-bold text-white">
48
- {brand.letter}
49
- </span>
50
- <span className="text-[15px] font-semibold tracking-tight">
51
- {brand.name} <span className="text-zinc-400">/ newsletter</span>
52
- </span>
53
- </div>
54
- <div className="flex items-center gap-4">
45
+ <div className="font-display flex min-h-screen flex-col bg-paper text-ink">
46
+ <header className="border-b border-rule">
47
+ <div className="mx-auto flex h-16 w-full max-w-4xl items-center justify-between px-6">
48
+ <Link href="/" className="flex items-center gap-3">
49
+ <img src="/images/headshot.jpg" alt="" className="size-8 rounded-full object-cover" />
50
+ <span className="text-[17px] font-medium text-ink">{brand.name}</span>
51
+ <span className="hidden text-[15px] italic text-ink-2 sm:inline">{newsletter.name}</span>
52
+ </Link>
53
+ <div className="flex items-center gap-5">
55
54
  <Link
56
55
  href="/"
57
- className="text-[13px] text-zinc-500 transition-colors hover:text-zinc-900"
56
+ className="inline-flex items-center gap-1 text-[15px] text-ink-2 transition-colors hover:text-ink"
58
57
  >
59
- View site ↗
58
+ View site
59
+ <ArrowUpRight aria-hidden className="size-4" />
60
60
  </Link>
61
61
  <UserMenu email={email} />
62
62
  </div>
63
63
  </div>
64
64
  </header>
65
65
  <main className="flex-1">
66
- <div className="mx-auto max-w-4xl px-6 py-8">{children}</div>
66
+ <div className="mx-auto w-full max-w-4xl px-6 py-10 sm:py-14">{children}</div>
67
67
  </main>
68
68
  </div>
69
69
  );
@@ -0,0 +1,2 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64"><circle cx="32" cy="32" r="31" fill="#4a6b3f"/>
2
+ <path d="M17 45V20h5.5L32 34l9.5-14H47v25h-6V31l-9 13-9-13v14z" fill="#f7f3ec"/></svg>
@@ -18,21 +18,21 @@ export default function LoginPage({ auth, response }: PageProps) {
18
18
  if (auth.user_id && !auth.user_id.startsWith("guest_")) response.redirect("/dashboard");
19
19
  const { brand } = siteConfig;
20
20
  return (
21
- <div className="flex min-h-screen items-center justify-center bg-white px-6 py-12">
22
- <div className="w-full max-w-[400px] rounded-2xl border border-zinc-200/70 p-8">
23
- <Link href="/" className="inline-flex">
24
- <span className="flex size-9 items-center justify-center rounded-xl bg-zinc-900 text-base font-bold text-white">
25
- {brand.letter}
26
- </span>
21
+ <div className="font-display flex min-h-screen flex-col bg-paper px-6 text-ink">
22
+ <div className="mx-auto flex h-16 w-full max-w-[40rem] items-center">
23
+ <Link href="/" className="text-[17px] font-medium text-ink">
24
+ {brand.name}
27
25
  </Link>
28
- <h1 className="mt-5 text-[22px] font-semibold tracking-tight text-zinc-900">
29
- {brand.name} dashboard
30
- </h1>
31
- <p className="mt-1 text-[13px] text-zinc-500">
32
- Sign in to see your subscribers.
33
- </p>
34
- <div className="mt-6">
35
- <AuthForm />
26
+ </div>
27
+ <div className="flex flex-1 items-center justify-center pb-24">
28
+ <div className="w-full max-w-[380px]">
29
+ <h1 className="text-[2rem] font-medium leading-tight">Sign in</h1>
30
+ <p className="mt-2 text-[17px] italic text-ink-2">
31
+ Owner access to {siteConfig.newsletter.name} subscribers.
32
+ </p>
33
+ <div className="mt-8 rounded-lg border border-rule bg-white p-6">
34
+ <AuthForm />
35
+ </div>
36
36
  </div>
37
37
  </div>
38
38
  </div>
@@ -41,8 +41,8 @@ const Subscriber = entity(
41
41
  );
42
42
 
43
43
  // A single-row, PII-FREE aggregate the public page can safely read live. It
44
- // holds only the subscriber count — no emails. `subscribe` keeps it in sync with
45
- // the real Subscriber count on every new join. The landing page subscribes with
44
+ // holds only the subscriber count — no emails. Every Subscriber writer
45
+ // recounts into it (lib/stats.ts → syncSubscriberCount). The landing page subscribes with
46
46
  // `db.useQuery("SubscriberCount")`, which syncs across every open tab through the
47
47
  // replica — so the counter ticks up everywhere the instant someone joins. This
48
48
  // is the cross-tab-safe realtime primitive (entity sync), not a per-connection
@@ -78,7 +78,7 @@ const User = entity(
78
78
  // PRIVACY. Subscriber holds visitor emails, so it denies EVERY client read and
79
79
  // write. No `db.useQuery("Subscriber")` can ever pull a row, and no client can
80
80
  // insert/update/delete directly. Writes happen only inside the server-side
81
- // `subscribe` mutation (functions bypass policies); the emails come back only
81
+ // `subscribe` and dev-only `seedDemo` mutations (functions bypass policies); the emails come back only
82
82
  // through the owner-gated `subscriberStats`. A marketing site must never leak
83
83
  // its own customers' emails — this policy is what guarantees it.
84
84
  const subscriberPolicy = policy({
@@ -92,7 +92,7 @@ const subscriberPolicy = policy({
92
92
 
93
93
  // The aggregate count is public to READ (it's just a number — the whole point
94
94
  // is that the landing page shows it live to everyone). Clients can't WRITE it;
95
- // only the subscribe mutation maintains it server-side.
95
+ // only the Subscriber writers maintain it server-side.
96
96
  const subscriberCountPolicy = policy({
97
97
  name: "subscriber_count_public_read",
98
98
  entity: "SubscriberCount",
@@ -0,0 +1,32 @@
1
+ import { mutation } from "@pylonsync/functions";
2
+ import { demoDataEnabled, demoSubscribers } from "../lib/demo";
3
+ import { syncSubscriberCount } from "../lib/stats";
4
+
5
+ // seedDemo — fill an empty subscriber list with fictional rows in development.
6
+ //
7
+ // The landing page counter calls this when it finds no subscribers, and the
8
+ // dashboard calls it once on load. It writes nothing unless demo data is on
9
+ // (lib/demo.ts: PYLON_DEMO_DATA, else a `pylon dev` process), so `pylon start`,
10
+ // Docker, and Pylon Cloud deploys do not get demo rows. It also writes nothing
11
+ // once any Subscriber exists, so it cannot mix demo rows into a real list.
12
+ // Clearing every subscriber in development seeds the list again on the next
13
+ // load. The advisory lock stops two first visits from seeding twice.
14
+ //
15
+ // Public because the first visitor in `pylon dev` is anonymous. It returns only
16
+ // a flag, never a row.
17
+ export default mutation<Record<string, never>, { seeded: boolean }>({
18
+ auth: "public",
19
+ async handler(ctx) {
20
+ if (!demoDataEnabled(ctx.env)) return { seeded: false };
21
+
22
+ await ctx.db.advisoryLock("creator_seed_demo");
23
+ const existing = await ctx.db.unsafe.list("Subscriber");
24
+ if (existing.length > 0) return { seeded: false };
25
+
26
+ for (const row of demoSubscribers(Date.now())) {
27
+ await ctx.db.unsafe.insert("Subscriber", { email: row.email, createdAt: row.createdAt });
28
+ }
29
+ await syncSubscriberCount(ctx.db.unsafe);
30
+ return { seeded: true };
31
+ },
32
+ });
@@ -1,4 +1,5 @@
1
1
  import { mutation, v } from "@pylonsync/functions";
2
+ import { syncSubscriberCount } from "../lib/stats";
2
3
 
3
4
  // Normalize + sanity-check an email without pulling in a dependency. This is a
4
5
  // pragmatic "looks like an email" check, not RFC 5322 — the unique index is the
@@ -61,21 +62,9 @@ export default mutation<{ email: string }, { ok: boolean; alreadyJoined: boolean
61
62
  throw e;
62
63
  }
63
64
 
64
- // Keep the public, PII-free SubscriberCount singleton in sync with the real
65
- // count. We RECOUNT (rather than +1) so the number can never drift, and
66
- // this whole handler is one transaction — on SQLite writers serialize, so
67
- // the recount-then-write is consistent. The landing page reads this row via
68
- // `db.useQuery`, which syncs the new value to every open tab.
69
- const total = (await ctx.db.unsafe.list("Subscriber")).length;
70
- const stat = (await ctx.db.unsafe.list("SubscriberCount"))[0] as
71
- | { id: string }
72
- | undefined;
73
- const now = new Date().toISOString();
74
- if (stat) {
75
- await ctx.db.unsafe.update("SubscriberCount", stat.id, { count: total, updatedAt: now });
76
- } else {
77
- await ctx.db.unsafe.insert("SubscriberCount", { count: total, updatedAt: now });
78
- }
65
+ // Keep the public, PII-free SubscriberCount row equal to the Subscriber
66
+ // count. It is a recount, not +1, so the number cannot drift from the table.
67
+ await syncSubscriberCount(ctx.db.unsafe);
79
68
 
80
69
  return { ok: true, alreadyJoined: false };
81
70
  },