@pylonsync/create-pylon 0.19.0 → 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,6 +3,7 @@
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 { Check, LogOut, MapPin, X } from "lucide-react";
6
7
  import { siteConfig } from "@/lib/site.config";
7
8
  import type { BookingRow, OwnerBookingsResult } from "@/lib/booking";
8
9
 
@@ -36,6 +37,12 @@ export function BookingsDashboard({ userEmail }: { userEmail: string }) {
36
37
  }
37
38
  }
38
39
 
40
+ // In `pylon dev` an empty booking book gets demo appointments
41
+ // (functions/seedDemo.ts). On a deploy the call returns without writing.
42
+ useEffect(() => {
43
+ void callFn("seedDemo", {}).catch(() => {});
44
+ }, []);
45
+
39
46
  useEffect(() => {
40
47
  void load();
41
48
  // Re-fetch whenever the live booked-slot set changes.
@@ -54,11 +61,7 @@ export function BookingsDashboard({ userEmail }: { userEmail: string }) {
54
61
 
55
62
  if (denied) return <OwnerOnly email={userEmail} />;
56
63
  if (error) {
57
- return (
58
- <div className="rounded-xl border border-red-200 bg-red-50 px-5 py-4 text-sm text-red-700">
59
- {error}
60
- </div>
61
- );
64
+ return <div className="border border-brand bg-brand-soft px-5 py-4 text-sm text-brand">{error}</div>;
62
65
  }
63
66
  if (!bookings) return <Skeleton />;
64
67
 
@@ -71,35 +74,59 @@ export function BookingsDashboard({ userEmail }: { userEmail: string }) {
71
74
  (b) => new Date(b.startsAt).toLocaleDateString() === todayKey,
72
75
  ).length;
73
76
  const pendingCount = upcoming.filter((b) => b.status === "pending").length;
77
+ const weekEnd = nowMs + 7 * 86_400_000;
78
+ const weekRevenue = upcoming
79
+ .filter((b) => Date.parse(b.startsAt) < weekEnd)
80
+ .reduce((sum, b) => sum + priceOf(b.serviceSlug), 0);
74
81
 
75
82
  return (
76
- <div className="space-y-8">
77
- <div>
78
- <h1 className="text-xl font-semibold tracking-tight">Bookings</h1>
79
- <p className="mt-1 text-sm text-zinc-500">
80
- Live — new bookings appear the moment they happen; cancelling frees the
81
- slot on the site instantly.
83
+ <div className="space-y-10">
84
+ <div className="flex flex-col gap-3 sm:flex-row sm:items-end sm:justify-between">
85
+ <div>
86
+ <h1 className="font-display text-[3.5rem] leading-none sm:text-[4.5rem]">Bookings</h1>
87
+ <p className="mt-3 max-w-lg text-[15px] leading-relaxed text-ink/70">
88
+ New bookings appear here as they are made. Cancelling a booking frees the time on
89
+ the site right away.
90
+ </p>
91
+ </div>
92
+ <p className="flex items-center gap-2 text-[13px] font-medium">
93
+ <span className="relative flex size-2">
94
+ <span className="absolute inline-flex size-2 animate-ping rounded-full bg-green-500/60" />
95
+ <span className="relative inline-flex size-2 rounded-full bg-green-600" />
96
+ </span>
97
+ Live
82
98
  </p>
83
99
  </div>
84
100
 
85
- <div className="grid gap-4 sm:grid-cols-3">
86
- <Stat label="Upcoming" value={upcoming.length} />
87
- <Stat label="Today" value={todayCount} />
88
- <Stat label="Awaiting confirm" value={pendingCount} />
89
- </div>
101
+ <dl className="grid grid-cols-2 gap-px border border-ink bg-ink sm:grid-cols-4">
102
+ <Stat label="Upcoming" value={String(upcoming.length)} />
103
+ <Stat label="Today" value={String(todayCount)} />
104
+ <Stat label="To confirm" value={String(pendingCount)} accent={pendingCount > 0} />
105
+ <Stat label="Booked, next 7 days" value={`$${weekRevenue.toLocaleString("en-US")}`} />
106
+ </dl>
90
107
 
91
108
  <UpcomingList bookings={upcoming} busyId={busyId} onAct={act} />
109
+
110
+ {!siteConfig.location.mapEmbedUrl ? <MapHint /> : null}
92
111
  </div>
93
112
  );
94
113
  }
95
114
 
96
- function Stat({ label, value }: { label: string; value: number }) {
115
+ function priceOf(slug: string): number {
116
+ return siteConfig.services.items.find((s) => s.slug === slug)?.priceUsd ?? 0;
117
+ }
118
+
119
+ function Stat({ label, value, accent }: { label: string; value: string; accent?: boolean }) {
97
120
  return (
98
- <div className="rounded-xl border border-zinc-200 bg-white p-4">
99
- <div className="text-[11px] font-medium uppercase tracking-wide text-zinc-400">
100
- {label}
101
- </div>
102
- <div className="mt-1 text-2xl font-semibold tabular-nums text-zinc-900">{value}</div>
121
+ <div className="bg-cream px-4 py-4">
122
+ <dt className="text-[11px] font-medium uppercase tracking-wide text-ink/60">{label}</dt>
123
+ <dd
124
+ className={
125
+ "mt-1 font-display text-[2.75rem] leading-none tabular-nums " + (accent ? "text-brand" : "")
126
+ }
127
+ >
128
+ {value}
129
+ </dd>
103
130
  </div>
104
131
  );
105
132
  }
@@ -117,7 +144,7 @@ function UpcomingList({
117
144
  const groups = useMemo(() => {
118
145
  const m = new Map<string, BookingRow[]>();
119
146
  for (const b of bookings) {
120
- const key = new Date(b.startsAt).toLocaleDateString(undefined, {
147
+ const key = new Date(b.startsAt).toLocaleDateString("en-US", {
121
148
  weekday: "long",
122
149
  month: "short",
123
150
  day: "numeric",
@@ -131,29 +158,28 @@ function UpcomingList({
131
158
 
132
159
  if (bookings.length === 0) {
133
160
  return (
134
- <div className="rounded-xl border border-dashed border-zinc-300 p-10 text-center text-sm text-zinc-500">
135
- No upcoming bookings yet. Share your site — new bookings land here live.
161
+ <div className="border border-dashed border-ink/50 p-10 text-center text-[15px] text-ink/70">
162
+ No upcoming bookings. New bookings appear here as soon as someone books a chair.
136
163
  </div>
137
164
  );
138
165
  }
139
166
 
140
167
  return (
141
- <div className="space-y-6">
168
+ <div className="space-y-8">
142
169
  {groups.map(([day, items]) => (
143
- <div key={day}>
144
- <h2 className="mb-2 text-[13px] font-semibold text-zinc-900">{day}</h2>
145
- <div className="overflow-hidden rounded-xl border border-zinc-200">
146
- {items.map((b, i) => (
147
- <BookingItem
148
- key={b.id}
149
- booking={b}
150
- busy={busyId === b.id}
151
- onAct={onAct}
152
- last={i === items.length - 1}
153
- />
154
- ))}
170
+ <section key={day}>
171
+ <div className="flex items-baseline justify-between border-b border-ink pb-2">
172
+ <h2 className="font-display text-[2rem] leading-none">{day}</h2>
173
+ <span className="text-[13px] text-ink/60">
174
+ {items.length} {items.length === 1 ? "booking" : "bookings"}
175
+ </span>
155
176
  </div>
156
- </div>
177
+ <ul className="divide-y divide-ink/20">
178
+ {items.map((b) => (
179
+ <BookingItem key={b.id} booking={b} busy={busyId === b.id} onAct={onAct} />
180
+ ))}
181
+ </ul>
182
+ </section>
157
183
  ))}
158
184
  </div>
159
185
  );
@@ -163,37 +189,29 @@ function BookingItem({
163
189
  booking,
164
190
  busy,
165
191
  onAct,
166
- last,
167
192
  }: {
168
193
  booking: BookingRow;
169
194
  busy: boolean;
170
195
  onAct: (id: string, fn: "confirmBooking" | "cancelBooking") => void;
171
- last: boolean;
172
196
  }) {
173
197
  const service = siteConfig.services.items.find((s) => s.slug === booking.serviceSlug);
174
198
  return (
175
- <div
176
- className={
177
- "flex flex-wrap items-center gap-x-4 gap-y-2 px-4 py-3 " +
178
- (last ? "" : "border-b border-zinc-100")
179
- }
180
- >
181
- <div className="w-20 shrink-0 text-[14px] font-semibold tabular-nums text-zinc-900">
182
- {new Date(booking.startsAt).toLocaleTimeString(undefined, {
199
+ <li className="flex flex-wrap items-center gap-x-5 gap-y-2 py-4">
200
+ <div className="w-24 shrink-0 font-display text-[1.75rem] leading-none tabular-nums">
201
+ {new Date(booking.startsAt).toLocaleTimeString("en-US", {
183
202
  hour: "numeric",
184
203
  minute: "2-digit",
185
204
  })}
186
205
  </div>
187
206
  <div className="min-w-0 flex-1">
188
- <div className="flex items-center gap-2">
189
- <span className="text-[14px] font-medium text-zinc-900">
190
- {booking.customerName}
191
- </span>
192
- <StatusBadge status={booking.status} />
207
+ <div className="flex flex-wrap items-center gap-2">
208
+ <span className="text-[15px] font-semibold">{booking.customerName}</span>
209
+ <StatusTag status={booking.status} />
193
210
  </div>
194
- <div className="truncate text-[12.5px] text-zinc-500">
195
- {service?.name ?? booking.serviceSlug} · {booking.customerEmail}
196
- {booking.customerPhone ? ` · ${booking.customerPhone}` : ""}
211
+ <div className="mt-0.5 truncate text-[13px] text-ink/65">
212
+ {service ? `${service.name} · ${service.durationMin} min · ${service.price}` : booking.serviceSlug}
213
+ {" · "}
214
+ {booking.customerPhone ?? booking.customerEmail}
197
215
  </div>
198
216
  </div>
199
217
  <div className="flex items-center gap-2">
@@ -202,35 +220,48 @@ function BookingItem({
202
220
  type="button"
203
221
  disabled={busy}
204
222
  onClick={() => onAct(booking.id, "confirmBooking")}
205
- className="rounded-md bg-zinc-900 px-3 py-1.5 text-[12.5px] font-medium text-white transition-colors hover:bg-zinc-700 disabled:opacity-50"
223
+ className="inline-flex h-9 items-center gap-1.5 bg-ink px-3.5 text-[13px] font-medium text-cream transition-opacity hover:opacity-85 disabled:opacity-50"
206
224
  >
207
- {busy ? "…" : "Confirm"}
225
+ <Check aria-hidden className="size-3.5" />
226
+ Confirm
208
227
  </button>
209
228
  ) : null}
210
229
  <button
211
230
  type="button"
212
231
  disabled={busy}
213
232
  onClick={() => onAct(booking.id, "cancelBooking")}
214
- className="rounded-md border border-zinc-300 px-3 py-1.5 text-[12.5px] font-medium text-zinc-600 transition-colors hover:border-red-300 hover:text-red-600 disabled:opacity-50"
233
+ className="inline-flex h-9 items-center gap-1.5 border border-ink/40 px-3.5 text-[13px] font-medium text-ink/75 transition-colors hover:border-brand hover:text-brand disabled:opacity-50"
215
234
  >
235
+ <X aria-hidden className="size-3.5" />
216
236
  Cancel
217
237
  </button>
218
238
  </div>
219
- </div>
239
+ </li>
220
240
  );
221
241
  }
222
242
 
223
- function StatusBadge({ status }: { status: string }) {
243
+ function StatusTag({ status }: { status: string }) {
224
244
  const tone =
225
245
  status === "confirmed"
226
- ? "bg-green-50 text-green-700"
246
+ ? "border-ink/30 text-ink/70"
227
247
  : status === "cancelled"
228
- ? "bg-zinc-100 text-zinc-500"
229
- : "bg-amber-50 text-amber-700";
248
+ ? "border-ink/20 text-ink/40"
249
+ : "border-brand bg-brand-soft text-brand";
250
+ const label = status === "pending" ? "To confirm" : status === "confirmed" ? "Confirmed" : "Cancelled";
251
+ return <span className={"border px-1.5 py-px text-[11px] font-medium " + tone}>{label}</span>;
252
+ }
253
+
254
+ // Setup notes for the owner live here, not on the public site.
255
+ function MapHint() {
230
256
  return (
231
- <span className={"rounded-full px-2 py-0.5 text-[10px] font-medium capitalize " + tone}>
232
- {status}
233
- </span>
257
+ <aside className="flex gap-3 border border-ink/30 bg-paper/60 p-4 text-[13.5px] leading-relaxed text-ink/75">
258
+ <MapPin aria-hidden className="mt-0.5 size-4 shrink-0 text-ink" />
259
+ <p>
260
+ Your site shows the weekly hours next to the address. To show a map there instead, paste a
261
+ Google Maps embed URL into <code className="bg-cream px-1 text-[12.5px]">location.mapEmbedUrl</code> in{" "}
262
+ <code className="bg-cream px-1 text-[12.5px]">lib/site.config.ts</code>.
263
+ </p>
264
+ </aside>
234
265
  );
235
266
  }
236
267
 
@@ -238,18 +269,16 @@ function StatusBadge({ status }: { status: string }) {
238
269
 
239
270
  function OwnerOnly({ email }: { email: string }) {
240
271
  return (
241
- <div className="rounded-xl border border-dashed border-zinc-300 px-6 py-12 text-center">
242
- <h1 className="text-lg font-semibold">This dashboard is owner-only</h1>
243
- <p className="mx-auto mt-2 max-w-md text-sm text-zinc-500">
244
- You&apos;re signed in as{" "}
245
- <span className="font-medium text-zinc-700">{email || "this account"}</span>.
272
+ <div className="border border-dashed border-ink/50 px-6 py-12 text-center">
273
+ <h1 className="font-display text-[2.5rem] leading-none">This dashboard is owner-only</h1>
274
+ <p className="mx-auto mt-4 max-w-md text-[14px] leading-relaxed text-ink/70">
275
+ You are signed in as <span className="font-medium text-ink">{email || "this account"}</span>.
246
276
  Only the shop owner can see bookings. Set{" "}
247
- <code className="rounded bg-zinc-100 px-1.5 py-0.5 text-[12px]">
277
+ <code className="bg-paper px-1.5 py-0.5 text-[12px]">
248
278
  PYLON_OWNER_EMAIL={email || "you@yourshop.com"}
249
279
  </code>{" "}
250
- in your{" "}
251
- <code className="rounded bg-zinc-100 px-1.5 py-0.5 text-[12px]">.env</code>,
252
- restart, and reload — or sign in with the owner account.
280
+ in <code className="bg-paper px-1.5 py-0.5 text-[12px]">.env</code>, restart, and reload. Or
281
+ sign in with the owner account.
253
282
  </p>
254
283
  </div>
255
284
  );
@@ -266,20 +295,22 @@ export function UserMenu({ email }: { email: string }) {
266
295
  }
267
296
  return (
268
297
  <details className="group relative">
269
- <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">
298
+ <summary
299
+ aria-label="Account"
300
+ className="flex size-9 cursor-pointer select-none list-none items-center justify-center bg-brand font-display text-[20px] leading-none text-cream marker:hidden [&::-webkit-details-marker]:hidden"
301
+ >
270
302
  {initial}
271
303
  </summary>
272
- <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)]">
273
- <div className="border-b border-zinc-100 px-3 py-2">
274
- <div className="truncate text-[13px] font-medium text-zinc-900">
275
- {email || "Signed in"}
276
- </div>
304
+ <div className="absolute right-0 top-full z-40 mt-2 w-60 border border-ink bg-cream py-1">
305
+ <div className="truncate border-b border-ink/20 px-3 py-2.5 text-[13px] font-medium">
306
+ {email || "Signed in"}
277
307
  </div>
278
308
  <button
279
309
  type="button"
280
310
  onClick={onSignOut}
281
- className="flex w-full items-center px-3 py-2 text-left text-[13px] text-zinc-700 transition-colors hover:bg-zinc-50"
311
+ className="flex w-full items-center gap-2 px-3 py-2 text-left text-[13px] text-ink/75 transition-colors hover:bg-paper hover:text-ink"
282
312
  >
313
+ <LogOut aria-hidden className="size-3.5" />
283
314
  Sign out
284
315
  </button>
285
316
  </div>
@@ -291,14 +322,10 @@ export function UserMenu({ email }: { email: string }) {
291
322
 
292
323
  function Skeleton() {
293
324
  return (
294
- <div className="space-y-8">
295
- <div className="h-6 w-32 animate-pulse rounded bg-zinc-100" />
296
- <div className="grid gap-4 sm:grid-cols-3">
297
- {[0, 1, 2].map((i) => (
298
- <div key={i} className="h-20 animate-pulse rounded-xl bg-zinc-100" />
299
- ))}
300
- </div>
301
- <div className="h-48 animate-pulse rounded-xl bg-zinc-100" />
325
+ <div className="space-y-10">
326
+ <div className="h-16 w-64 animate-pulse bg-ink/10" />
327
+ <div className="h-24 animate-pulse bg-ink/10" />
328
+ <div className="h-72 animate-pulse bg-ink/10" />
302
329
  </div>
303
330
  );
304
331
  }
@@ -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, BookingsDashboard } from "./dashboard-client";
5
6
 
@@ -30,33 +31,32 @@ export default function DashboardPage({ auth, response, serverData }: PageProps)
30
31
  );
31
32
  }
32
33
 
34
+ // Dashboard chrome in the site's own look: the cream page, the ruled header,
35
+ // the shop name in the display face, a link back to the site, and the account
36
+ // menu (a client island for sign-out).
33
37
  function Shell({ email, children }: { email: string; children: React.ReactNode }) {
34
38
  const { brand } = siteConfig;
35
39
  return (
36
- <div className="flex min-h-screen flex-col bg-white text-zinc-900">
37
- <header className="border-b border-zinc-200">
38
- <div className="mx-auto flex h-14 max-w-4xl items-center justify-between px-6">
39
- <div className="flex items-center gap-2">
40
- <span className="flex size-6 items-center justify-center rounded-[7px] bg-zinc-900 text-[13px] font-bold text-white">
41
- {brand.letter}
42
- </span>
43
- <span className="text-[15px] font-semibold tracking-tight">
44
- {brand.name} <span className="text-zinc-400">/ bookings</span>
45
- </span>
46
- </div>
40
+ <div className="flex min-h-screen flex-col bg-cream text-ink">
41
+ <header className="sticky top-0 z-30 border-b border-ink bg-cream">
42
+ <div className="mx-auto flex h-14 w-full max-w-5xl items-center justify-between px-6">
43
+ <Link href="/" className="font-display text-[26px] leading-none tracking-[0.02em]">
44
+ {brand.name}
45
+ </Link>
47
46
  <div className="flex items-center gap-4">
48
47
  <Link
49
48
  href="/"
50
- className="text-[13px] text-zinc-500 transition-colors hover:text-zinc-900"
49
+ className="hidden items-center gap-1 text-[13px] font-medium text-ink/70 transition-colors hover:text-ink sm:inline-flex"
51
50
  >
52
- View site ↗
51
+ View site
52
+ <ArrowUpRight aria-hidden className="size-3.5" />
53
53
  </Link>
54
54
  <UserMenu email={email} />
55
55
  </div>
56
56
  </div>
57
57
  </header>
58
58
  <main className="flex-1">
59
- <div className="mx-auto max-w-4xl px-6 py-8">{children}</div>
59
+ <div className="mx-auto w-full max-w-5xl px-6 py-10 sm:py-12">{children}</div>
60
60
  </main>
61
61
  </div>
62
62
  );
@@ -0,0 +1,2 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64"><rect width="64" height="64" rx="6" fill="#9b1c1c"/>
2
+ <path d="M18 12h8.5l11 24V12H46v40h-8.5l-11-24v24H18z" fill="#f4efe6"/></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>
27
- </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 manage your bookings.
33
- </p>
34
- <div className="mt-6">
35
- <AuthForm />
21
+ <div className="flex min-h-screen flex-col bg-cream text-ink">
22
+ <div className="border-b border-ink">
23
+ <div className="mx-auto flex h-14 w-full max-w-5xl items-center px-6">
24
+ <Link href="/" className="font-display text-[26px] leading-none tracking-[0.02em]">
25
+ {brand.name}
26
+ </Link>
27
+ </div>
28
+ </div>
29
+ <div className="flex flex-1 items-center justify-center px-6 py-16">
30
+ <div className="w-full max-w-[400px]">
31
+ <h1 className="font-display text-[3.5rem] leading-none">Owner sign in</h1>
32
+ <p className="mt-3 text-[15px] text-ink/70">Bookings, confirmations, and cancellations.</p>
33
+ <div className="mt-8 border border-ink bg-paper/60 p-6">
34
+ <AuthForm />
35
+ </div>
36
36
  </div>
37
37
  </div>
38
38
  </div>
@@ -0,0 +1,49 @@
1
+ import { mutation } from "@pylonsync/functions";
2
+ import { demoBookings, demoDataEnabled } from "../lib/demo";
3
+ import { siteConfig } from "../lib/site.config";
4
+
5
+ // seedDemo — fill an empty booking book with fictional appointments in
6
+ // development, so the public grid and the owner dashboard show the same
7
+ // bookings.
8
+ //
9
+ // The booking widget calls this when it finds no booked slots, and the
10
+ // dashboard calls it once on load. It writes nothing unless demo data is on
11
+ // (lib/demo.ts: PYLON_DEMO_DATA, else a `pylon dev` process), so `pylon start`,
12
+ // Docker, and Pylon Cloud deploys do not get demo rows. It also writes nothing
13
+ // once any Booking exists, so it cannot mix demo rows into a real book. The
14
+ // advisory lock stops two first visits from seeding twice.
15
+ //
16
+ // Each active booking gets its BookedSlot, the same pair createBooking writes.
17
+ // Public because the first visitor in `pylon dev` is anonymous. It returns only
18
+ // a flag, never a row.
19
+ export default mutation<Record<string, never>, { seeded: boolean }>({
20
+ auth: "public",
21
+ async handler(ctx) {
22
+ if (!demoDataEnabled(ctx.env)) return { seeded: false };
23
+
24
+ await ctx.db.advisoryLock("local_service_seed_demo");
25
+ const existing = await ctx.db.unsafe.list("Booking");
26
+ if (existing.length > 0) return { seeded: false };
27
+
28
+ for (const b of demoBookings(Date.now(), siteConfig)) {
29
+ const bookingId = await ctx.db.unsafe.insert("Booking", {
30
+ serviceSlug: b.serviceSlug,
31
+ startsAt: b.startsAt,
32
+ endsAt: b.endsAt,
33
+ customerName: b.customerName,
34
+ customerEmail: b.customerEmail,
35
+ customerPhone: b.customerPhone,
36
+ status: b.status,
37
+ createdAt: b.createdAt,
38
+ });
39
+ if (b.status === "cancelled") continue;
40
+ await ctx.db.unsafe.insert("BookedSlot", {
41
+ serviceSlug: b.serviceSlug,
42
+ startsAt: b.startsAt,
43
+ endsAt: b.endsAt,
44
+ bookingId,
45
+ });
46
+ }
47
+ return { seeded: true };
48
+ },
49
+ });
@@ -0,0 +1,135 @@
1
+ // Demo bookings for local development.
2
+ //
3
+ // `pylon dev` fills an empty booking book with fictional appointments from the
4
+ // past week and the next ten days, so the booking grid shows taken times and
5
+ // the owner dashboard has a schedule. Both read the same rows: every demo
6
+ // Booking gets its BookedSlot, exactly like createBooking writes.
7
+ //
8
+ // A deploy does not seed: `demoDataEnabled` is true only when PYLON_DEMO_DATA is
9
+ // on, or when it is unset and the process runs under `pylon dev`. Set PYLON_DEMO_DATA=0 to
10
+ // turn it off in development too.
11
+ //
12
+ // Pure data + pure shaping, so functions/seedDemo.ts stays a thin wrapper and
13
+ // this file is testable without a server.
14
+
15
+ import { localDateKey, rangesOverlap, slotsForDay, weekdayOf } from "./slots";
16
+ import type { LocalServiceConfig } from "./site.config";
17
+
18
+ const ON = new Set(["1", "true", "yes", "on"]);
19
+ const OFF = new Set(["0", "false", "no", "off"]);
20
+
21
+ /**
22
+ * True when demo rows may be written. An explicit PYLON_DEMO_DATA wins.
23
+ * Otherwise it needs both PYLON_DEV_MODE on and PYLON_DEV_WATCH_DIR set. Only
24
+ * `pylon dev` sets PYLON_DEV_WATCH_DIR; `pylon start` and the Docker image do
25
+ * not, and the image turns PYLON_DEV_MODE on by default, so dev mode alone is
26
+ * not proof of a local machine.
27
+ */
28
+ export function demoDataEnabled(env: Record<string, string | undefined>): boolean {
29
+ const explicit = env.PYLON_DEMO_DATA?.trim().toLowerCase();
30
+ if (explicit && ON.has(explicit)) return true;
31
+ if (explicit && OFF.has(explicit)) return false;
32
+ const dev = env.PYLON_DEV_MODE?.trim().toLowerCase();
33
+ const devMode = dev === "1" || dev === "true";
34
+ return devMode && Boolean(env.PYLON_DEV_WATCH_DIR?.trim());
35
+ }
36
+
37
+ const CUSTOMERS = [
38
+ "Marcus Bell", "Daniel Reyes", "Hannah Kim", "Luis Ortega", "Tyler Brooks", "Andre Wallace",
39
+ "Sam Okafor", "Kevin Tran", "Jordan Miles", "Eli Navarro", "Chris Delgado", "Owen Price",
40
+ "Ravi Shah", "Nate Coleman", "Isaac Moreno", "Ben Hollis", "Victor Lam", "Grant Ellison",
41
+ "Mateo Vargas", "Dev Patel", "Caleb Ford", "Jamal Hayes", "Theo Lindqvist", "Omar Haddad",
42
+ ];
43
+
44
+ /** Small deterministic PRNG (mulberry32) so the demo set is stable per seed. */
45
+ function rng(seed: number): () => number {
46
+ let a = seed >>> 0;
47
+ return () => {
48
+ a = (a + 0x6d2b79f5) >>> 0;
49
+ let t = a;
50
+ t = Math.imul(t ^ (t >>> 15), t | 1);
51
+ t ^= t + Math.imul(t ^ (t >>> 7), t | 61);
52
+ return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
53
+ };
54
+ }
55
+
56
+ export interface DemoBooking {
57
+ serviceSlug: string;
58
+ startsAt: string;
59
+ endsAt: string;
60
+ customerName: string;
61
+ customerEmail: string;
62
+ customerPhone: string | null;
63
+ status: "pending" | "confirmed" | "cancelled";
64
+ createdAt: string;
65
+ }
66
+
67
+ type Cfg = Pick<LocalServiceConfig, "booking" | "services">;
68
+
69
+ /**
70
+ * Appointments on open days from 7 days ago to 9 days ahead. Each fits inside
71
+ * opening hours on the slot grid, and no two active bookings overlap. Past
72
+ * appointments are confirmed; upcoming ones are mostly confirmed, some
73
+ * pending, a few cancelled.
74
+ */
75
+ export function demoBookings(nowMs: number, cfg: Cfg, seed = 11): DemoBooking[] {
76
+ const rand = rng(seed);
77
+ const out: DemoBooking[] = [];
78
+ const services = cfg.services.items;
79
+ if (services.length === 0) return out;
80
+
81
+ for (let offset = -7; offset <= 9; offset++) {
82
+ const day = localDateKey(offset, nowMs);
83
+ const hours = cfg.booking.hours[weekdayOf(day)];
84
+ if (!hours) continue;
85
+ // The full grid for the day, ignoring "now": seeding also fills the past.
86
+ const grid = slotsForDay({
87
+ dayISODate: day,
88
+ open: hours.open,
89
+ close: hours.close,
90
+ slotMinutes: cfg.booking.slotMinutes,
91
+ durationMin: 0,
92
+ leadTimeHours: 0,
93
+ busy: [],
94
+ nowMs: 0,
95
+ });
96
+ // With a zero duration the last grid start is closing time itself.
97
+ const close = Date.parse(grid[grid.length - 1].startsAt);
98
+ const busy: [number, number][] = [];
99
+ // Busier near the weekend and in the days just ahead.
100
+ const fill = offset >= 0 && offset <= 3 ? 0.5 : 0.35;
101
+ for (const slot of grid) {
102
+ if (rand() > fill) continue;
103
+ const svc = services[Math.floor(rand() * services.length)];
104
+ const start = Date.parse(slot.startsAt);
105
+ const end = start + svc.durationMin * 60_000;
106
+ if (end > close) continue;
107
+ if (busy.some(([s, e]) => rangesOverlap(start, end, s, e))) continue;
108
+
109
+ const name = CUSTOMERS[Math.floor(rand() * CUSTOMERS.length)];
110
+ const [first, last] = name.toLowerCase().split(" ");
111
+ const past = end <= nowMs;
112
+ const roll = rand();
113
+ const status: DemoBooking["status"] = past
114
+ ? "confirmed"
115
+ : roll < 0.62
116
+ ? "confirmed"
117
+ : roll < 0.92
118
+ ? "pending"
119
+ : "cancelled";
120
+ if (status !== "cancelled") busy.push([start, end]);
121
+ const bookedAt = Math.min(nowMs - 3_600_000, start - (1 + Math.floor(rand() * 6)) * 86_400_000);
122
+ out.push({
123
+ serviceSlug: svc.slug,
124
+ startsAt: new Date(start).toISOString(),
125
+ endsAt: new Date(end).toISOString(),
126
+ customerName: name,
127
+ customerEmail: `${first}.${last[0]}@example.com`,
128
+ customerPhone: rand() < 0.7 ? `(214) 555-01${String(Math.floor(rand() * 100)).padStart(2, "0")}` : null,
129
+ status,
130
+ createdAt: new Date(bookedAt).toISOString(),
131
+ });
132
+ }
133
+ }
134
+ return out;
135
+ }