@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
@@ -1,6 +1,6 @@
1
1
  "use client";
2
2
 
3
- // Shared client-side glue for Pylon Market: types, display helpers, and the
3
+ // Shared client-side glue for the marketplace: types, display helpers, and the
4
4
  // email/password auth bootstrap. Same-origin under native SSR, so no baseUrl —
5
5
  // init() resolves window.location.origin.
6
6
  import { init, configureClient, callFn, setSessionToken, storageKey } from "@pylonsync/react";
@@ -10,13 +10,13 @@ import { init, configureClient, callFn, setSessionToken, storageKey } from "@pyl
10
10
  // keyspace disagreement, no double token migration.
11
11
  export const APP_NAME = "__APP_NAME__";
12
12
 
13
- // Prefilled demo account — the shopper you sign in as. Owns a couple of its
14
- // own listings so "My Market" isn't empty, but NOT the bulk of the catalog,
15
- // so it can actually buy + bid on things.
13
+ // Prefilled demo account: the shopper you sign in as. Owns three listings (so
14
+ // the dashboard has offers to answer) but not the bulk of the catalog, so it
15
+ // can buy and bid on everything else.
16
16
  export const DEMO = {
17
17
  email: "demo@pylon.market",
18
18
  password: "pylondemo123",
19
- name: "Demo Shopper",
19
+ name: "Sam Rivera",
20
20
  } as const;
21
21
 
22
22
  // The seed seller that owns most of the catalog. Nobody logs in as this; it
@@ -27,13 +27,23 @@ const BAZAAR = {
27
27
  name: "Pylon Bazaar",
28
28
  } as const;
29
29
 
30
+ // A seed buyer. Nobody logs in as this; it sends the demo offers (including
31
+ // offers on the demo shopper's own listings) so every listing page and the
32
+ // dashboard have live offer activity on first boot.
33
+ const COLLECTOR = {
34
+ email: "collector@pylon.market",
35
+ password: "collectorseed123",
36
+ name: "Collector",
37
+ } as const;
38
+
30
39
  // How many of the seeded listings the bazaar owns; the rest go to the demo.
31
- const BAZAAR_COUNT = 10;
40
+ const BAZAAR_COUNT = 7;
32
41
 
33
42
  export interface Listing {
34
43
  id: string;
35
44
  sellerId: string;
36
45
  sellerName: string;
46
+ location?: string;
37
47
  title: string;
38
48
  slug: string;
39
49
  description: string;
@@ -85,12 +95,18 @@ function hash(s: string): number {
85
95
  return h >>> 0;
86
96
  }
87
97
 
88
- /** Deterministic visual fallback for listings whose remote image is unavailable. */
98
+ /** Deterministic visual fallback for listings without a photo. */
89
99
  export function gradient(seed: string): string {
90
100
  const h = hash(seed);
91
- const a = h % 360;
92
- const b = (a + 40 + ((h >> 3) % 90)) % 360;
93
- return `linear-gradient(135deg, hsl(${a} 68% 56%), hsl(${b} 72% 44%))`;
101
+ const a = 20 + (h % 60);
102
+ return `linear-gradient(160deg, hsl(${a} 22% 82%), hsl(${a + 12} 18% 64%))`;
103
+ }
104
+
105
+ /** A stable, muted avatar color for a person's name. */
106
+ export function avatarColor(name: string): string {
107
+ const hues = [18, 32, 96, 152, 196, 262, 340];
108
+ const hue = hues[hash(name) % hues.length];
109
+ return `hsl(${hue} 32% 42%)`;
94
110
  }
95
111
 
96
112
  export function initials(title: string): string {
@@ -305,15 +321,29 @@ async function ensureAccount(
305
321
  * prefilled login is one click from a working session that can actually buy
306
322
  * and sell. Idempotent and best-effort: any failure is swallowed.
307
323
  *
308
- * Two accounts are seeded:
324
+ * Three accounts are seeded:
309
325
  * - **bazaar** owns the bulk of the catalog, so the demo shopper has other
310
326
  * people's listings to buy + bid on (you can't buy your own);
311
- * - **demo** (the prefilled login) owns a couple of its own listings so
312
- * "My Market" has something in it.
327
+ * - **demo** (the prefilled login) owns three listings so the dashboard has
328
+ * something in it;
329
+ * - **collector** sends the demo offers and buys one item, so listings show
330
+ * offer activity and the demo shopper has offers to accept.
313
331
  *
314
- * Both tokens are used transiently to seed and then discarded — the visitor
332
+ * All tokens are used transiently to seed and then discarded — the visitor
315
333
  * stays anonymous (read-only) until they choose to sign in.
316
334
  */
335
+ /** True when the public entity API returns at least one row. */
336
+ async function hasAnyRow(entity: string): Promise<boolean> {
337
+ try {
338
+ const res = await fetch(`/api/entities/${entity}?limit=1`);
339
+ if (!res.ok) return false;
340
+ const body = (await res.json()) as { data?: unknown[] };
341
+ return (body.data?.length ?? 0) > 0;
342
+ } catch {
343
+ return false;
344
+ }
345
+ }
346
+
317
347
  // Shared in-flight promise so the many islands that mount MarketProvider on
318
348
  // one page (ticker, header, SeedOnEmpty) all await the SAME seed run instead
319
349
  // of each firing their own — that flood was tripping the login rate limit,
@@ -329,6 +359,14 @@ export function ensureDemoSeed(options: { force?: boolean } = {}): Promise<void>
329
359
  if (seedPromise) return seedPromise;
330
360
 
331
361
  seedPromise = (async () => {
362
+ // The collector's offers are the last seed step, so any Offer row means
363
+ // the accounts and catalog already exist. Skipping here keeps a fresh
364
+ // browser from spending the login rate limit on seed accounts.
365
+ if (!options.force && (await hasAnyRow("Offer"))) {
366
+ localStorage.setItem("market:demo-seeded", "1");
367
+ return;
368
+ }
369
+
332
370
  const bazaarToken = await ensureAccount(
333
371
  BAZAAR.email,
334
372
  BAZAAR.password,
@@ -347,6 +385,15 @@ export function ensureDemoSeed(options: { force?: boolean } = {}): Promise<void>
347
385
  await callFn("seedMarket", { start: BAZAAR_COUNT }, { token: demoToken });
348
386
  }
349
387
 
388
+ const collectorToken = await ensureAccount(
389
+ COLLECTOR.email,
390
+ COLLECTOR.password,
391
+ COLLECTOR.name,
392
+ );
393
+ if (collectorToken) {
394
+ await callFn("seedOffers", {}, { token: collectorToken });
395
+ }
396
+
350
397
  // Mark done only AFTER a successful pass, so a failed/partial run retries
351
398
  // on the next load instead of being permanently skipped.
352
399
  localStorage.setItem("market:demo-seeded", "1");
@@ -1,30 +1,141 @@
1
1
  import { mutation, v } from "@pylonsync/functions";
2
2
 
3
- // A handful of believable listings so the marketplace isn't empty on first
4
- // run. Idempotent: seeds only when the catalog is empty, so the browse page
5
- // can call it on load without worrying about duplicates.
3
+ // Demo catalog for first boot. The client bootstrap calls this twice: once as
4
+ // the "bazaar" account for DEMO[0..7] and once as the demo shopper for the
5
+ // rest, so the shopper has other people's listings to buy and a few of its
6
+ // own to receive offers on. Idempotent per caller: a seller that already has
7
+ // listings is skipped.
6
8
  const DEMO: Array<{
7
9
  seller: string;
10
+ location: string;
8
11
  title: string;
9
12
  description: string;
10
13
  price: number;
11
14
  category: string;
12
15
  condition: string;
13
- imageUrl: string;
16
+ photo: string;
14
17
  seed: string;
15
18
  }> = [
16
- { seller: "maple-fox", title: "Herman Miller Aeron (size B)", description: "Fully loaded, posture-fit SL. Light desk use, no squeaks.", price: 540, category: "furniture", condition: "like-new", imageUrl: "https://images.unsplash.com/photo-1758607010203-d6b7f6ec9262?auto=format&fit=crop&w=1200&q=82", seed: "a1f3" },
17
- { seller: "amber-lynx", title: "Kodak Retina IIa rangefinder", description: "1950s folding 35mm. Clean glass, accurate shutter, leather case.", price: 185, category: "cameras", condition: "good", imageUrl: "https://images.unsplash.com/photo-1516035069371-29a1b244cc32?auto=format&fit=crop&w=1200&q=82", seed: "b7c2" },
18
- { seller: "swift-otter", title: "Specialized Allez road bike, 54cm", description: "Shimano Claris, fresh bar tape, recently tuned. Fast commuter.", price: 420, category: "bikes", condition: "good", imageUrl: "https://images.unsplash.com/photo-1532298229144-0ec0c57515c7?auto=format&fit=crop&w=1200&q=82", seed: "c4e9" },
19
- { seller: "cosmic-wren", title: "Vintage Technics SL-1200 MK2", description: "Legendary direct-drive turntable. Spins true, new slipmat.", price: 650, category: "audio", condition: "good", imageUrl: "https://images.unsplash.com/photo-1599723895995-80e160d14fe4?auto=format&fit=crop&w=1200&q=82", seed: "d8a1" },
20
- { seller: "ivory-sparrow", title: "Le Creuset 5.5qt Dutch oven", description: "Flame orange, enamel intact. The one everyone wants.", price: 120, category: "kitchen", condition: "like-new", imageUrl: "https://images.unsplash.com/photo-1559137703-44aeed6161b6?auto=format&fit=crop&w=1200&q=82", seed: "e2b6" },
21
- { seller: "slate-heron", title: 'LG 27" 4K UltraFine monitor', description: "USB-C, 60Hz, color-accurate. Box + cables included.", price: 240, category: "electronics", condition: "good", imageUrl: "https://images.unsplash.com/photo-1527443224154-c4a3942d3acf?auto=format&fit=crop&w=1200&q=82", seed: "f5d0" },
22
- { seller: "maple-fox", title: "Eames-style lounge + ottoman", description: "Walnut + tan leather replica. Sturdy, very comfortable.", price: 380, category: "furniture", condition: "good", imageUrl: "https://images.unsplash.com/photo-1567538096630-e0c55bd6374c?auto=format&fit=crop&w=1200&q=82", seed: "a9c4" },
23
- { seller: "amber-lynx", title: "Patagonia Black Hole 55L duffel", description: "One trip old. Weatherproof, zips perfect. Forest green.", price: 75, category: "outdoor", condition: "like-new", imageUrl: "https://images.unsplash.com/photo-1699319656128-6e353d6b9c7c?auto=format&fit=crop&w=1200&q=82", seed: "b3f8" },
24
- { seller: "cosmic-wren", title: "Fender Player Stratocaster, sunburst", description: "Maple neck, plays great. Small buckle rash on back.", price: 560, category: "instruments", condition: "good", imageUrl: "https://images.unsplash.com/photo-1525201548942-d8732f6617a0?auto=format&fit=crop&w=1200&q=82", seed: "d1e7" },
25
- { seller: "slate-heron", title: "iPad Air (5th gen) 64GB", description: "Space gray, excellent battery health, screen flawless.", price: 330, category: "electronics", condition: "like-new", imageUrl: "https://images.unsplash.com/photo-1544244015-0df4b3ffc6b0?auto=format&fit=crop&w=1200&q=82", seed: "f0a2" },
26
- { seller: "ivory-sparrow", title: "Mid-century teak sideboard", description: "Three drawers, two cabinets. Real teak, refinished top.", price: 295, category: "furniture", condition: "good", imageUrl: "https://images.unsplash.com/photo-1558997519-83ea9252edf8?auto=format&fit=crop&w=1200&q=82", seed: "e7c3" },
27
- { seller: "swift-otter", title: "Brooks Brothers wool overcoat, 40R", description: "Charcoal herringbone, fully lined. Dry-cleaned, ready to wear.", price: 90, category: "apparel", condition: "good", imageUrl: "https://images.unsplash.com/photo-1551488831-00ddcb6c6bd3?auto=format&fit=crop&w=1200&q=82", seed: "c2b5" },
19
+ {
20
+ seller: "Nora Lind",
21
+ location: "Brooklyn, NY",
22
+ title: "Danish teak lounge chair, oatmeal wool",
23
+ description:
24
+ "Solid teak frame with the original joinery, oiled last spring. Cushions were reupholstered in a heavy oatmeal wool bouclé. No wobble, no cracks. Seat height 16 in, width 27 in. Pickup in Greenpoint or I can help arrange a courier.",
25
+ price: 680,
26
+ category: "furniture",
27
+ condition: "good",
28
+ photo: "lounge-chair",
29
+ seed: "a1f3",
30
+ },
31
+ {
32
+ seller: "Marcus Bell",
33
+ location: "Dallas, TX",
34
+ title: "1960s steel dress watch, cream dial",
35
+ description:
36
+ "34 mm stainless case, hand-wound movement serviced in March. Keeps about +6 s/day. Cream dial has light, even patina. Comes on a brown suede strap with a spare black leather strap.",
37
+ price: 890,
38
+ category: "watches",
39
+ condition: "good",
40
+ photo: "watch",
41
+ seed: "b7c2",
42
+ },
43
+ {
44
+ seller: "Theo Park",
45
+ location: "Portland, OR",
46
+ title: "Chrome 35mm SLR with 50mm f/1.7",
47
+ description:
48
+ "Fully mechanical body, shutter fires at every speed and sounds right. Light meter reads within a third of a stop. Lens is clean with no haze or fungus. Original leather strap. Tested with a roll of Portra; sample scans on request.",
49
+ price: 240,
50
+ category: "cameras",
51
+ condition: "good",
52
+ photo: "film-camera",
53
+ seed: "c4e9",
54
+ },
55
+ {
56
+ seller: "Ava Moreno",
57
+ location: "Austin, TX",
58
+ title: "Walnut direct-drive turntable",
59
+ description:
60
+ "Direct-drive deck in a solid walnut plinth. Speed is stable at 33 and 45, pitch control works. New belt-free motor service, new cartridge with about 40 hours on it. Dust cover not included.",
61
+ price: 410,
62
+ category: "audio",
63
+ condition: "like-new",
64
+ photo: "turntable",
65
+ seed: "d8a1",
66
+ },
67
+ {
68
+ seller: "Nora Lind",
69
+ location: "Brooklyn, NY",
70
+ title: "Cognac leather weekender",
71
+ description:
72
+ "Full-grain leather duffel with solid brass hardware and a detachable shoulder strap. Carried on about ten trips; the leather has softened and darkened evenly. Interior lining is clean. 20 x 11 x 10 in.",
73
+ price: 295,
74
+ category: "bags",
75
+ condition: "good",
76
+ photo: "weekender-bag",
77
+ seed: "a9c4",
78
+ },
79
+ {
80
+ seller: "Elise Carter",
81
+ location: "Asheville, NC",
82
+ title: "Speckled stoneware table lamp",
83
+ description:
84
+ "Hand-thrown stoneware base in a sand glaze, pleated linen shade. Rewired with a cloth cord and inline switch. 22 in tall with the shade. One tiny glaze pop near the foot, shown in photos.",
85
+ price: 165,
86
+ category: "lighting",
87
+ condition: "like-new",
88
+ photo: "ceramic-lamp",
89
+ seed: "b3f8",
90
+ },
91
+ {
92
+ seller: "Elise Carter",
93
+ location: "Asheville, NC",
94
+ title: "Tufted slipper chair, ivory",
95
+ description:
96
+ "Small button-tufted slipper chair in ivory cotton velvet on turned white legs. Frame is tight, fabric has no stains or pulls. Seat height 17 in, width 24 in. Works in a bedroom or reading corner.",
97
+ price: 240,
98
+ category: "furniture",
99
+ condition: "like-new",
100
+ photo: "slipper-chair",
101
+ seed: "m5t1",
102
+ },
103
+ {
104
+ seller: "Sam Rivera",
105
+ location: "Denver, CO",
106
+ title: "Two-door oak wardrobe",
107
+ description:
108
+ "Solid oak wardrobe with two doors over two deep drawers. Hanging rail and one shelf inside. Doors close flush, drawers run smoothly. 40 in wide, 22 in deep, 74 in tall. Comes apart at the top for moving.",
109
+ price: 450,
110
+ category: "furniture",
111
+ condition: "good",
112
+ photo: "wardrobe",
113
+ seed: "n8w3",
114
+ },
115
+ {
116
+ seller: "Sam Rivera",
117
+ location: "Denver, CO",
118
+ title: "Carbon road bike, 54 cm, matte black",
119
+ description:
120
+ "Carbon frame and fork, 54 cm, matte black. 2x11 drivetrain shifts cleanly, new chain and brake pads this season. No cracks or impact damage. About 8.4 kg as pictured.",
121
+ price: 1250,
122
+ category: "bikes",
123
+ condition: "good",
124
+ photo: "road-bike",
125
+ seed: "e2b6",
126
+ },
127
+ {
128
+ seller: "Sam Rivera",
129
+ location: "Denver, CO",
130
+ title: "Black travel duffel, 55 L",
131
+ description:
132
+ "Water-resistant nylon duffel with padded shoulder straps and a separate shoe pocket. Used on two trips. All zips and buckles work. Packs flat into its own pocket.",
133
+ price: 70,
134
+ category: "bags",
135
+ condition: "like-new",
136
+ photo: "duffel",
137
+ seed: "p2d6",
138
+ },
28
139
  ];
29
140
 
30
141
  interface SeedMarketArgs {
@@ -36,11 +147,17 @@ interface SeedMarketResult {
36
147
  seeded: number;
37
148
  }
38
149
 
150
+ function slugify(s: string): string {
151
+ return s
152
+ .toLowerCase()
153
+ .trim()
154
+ .replace(/[^a-z0-9]+/g, "-")
155
+ .replace(/^-+|-+$/g, "")
156
+ .slice(0, 60);
157
+ }
158
+
39
159
  export default mutation<SeedMarketArgs, SeedMarketResult>({
40
- // Defaults to auth: "user" — seeds a slice of the catalog owned by the
41
- // caller. The bootstrap calls this twice: once as a "bazaar" seller for the
42
- // bulk of the catalog (so the demo buyer can bid on it), and once as the
43
- // demo account for a couple of its own listings.
160
+ // auth defaults to "user": every seeded listing is owned by the caller.
44
161
  args: {
45
162
  start: v.optional(v.number()),
46
163
  end: v.optional(v.number()),
@@ -48,32 +165,22 @@ export default mutation<SeedMarketArgs, SeedMarketResult>({
48
165
  async handler(ctx, args) {
49
166
  if (!ctx.auth.userId) throw ctx.error("UNAUTHENTICATED", "sign in first");
50
167
 
51
- // Per-caller idempotency: skip if THIS seller already has listings, so the
52
- // two seed calls (and any reloads) don't duplicate.
53
- const all = await ctx.db.list("Listing") as Array<{ sellerId: string }>;
168
+ const all = (await ctx.db.list("Listing")) as Array<{ sellerId: string }>;
54
169
  if (all.some((l) => l.sellerId === ctx.auth.userId)) return { seeded: 0 };
55
170
 
56
171
  const start = args.start ?? 0;
57
172
  const end = args.end ?? DEMO.length;
58
173
  const slice = DEMO.slice(start, end);
59
174
 
60
- // Stagger createdAt so the grid + ticker have a believable order. The
61
- // seller id is the caller — `Listing.sellerId` is `field.owner()`, so the
62
- // framework would reject any other value. `seller` stays a display name.
63
- const slugify = (s: string) =>
64
- s
65
- .toLowerCase()
66
- .trim()
67
- .replace(/[^a-z0-9]+/g, "-")
68
- .replace(/^-+|-+$/g, "")
69
- .slice(0, 60);
70
-
175
+ // Stagger createdAt so "newest first" follows the array order. The
176
+ // seller id is the caller; `sellerName` is a display name only.
71
177
  const now = Date.now();
72
178
  let n = 0;
73
179
  for (const d of slice) {
74
180
  await ctx.db.insert("Listing", {
75
181
  sellerId: ctx.auth.userId,
76
182
  sellerName: d.seller,
183
+ location: d.location,
77
184
  title: d.title,
78
185
  slug: `${slugify(d.title) || "item"}-${d.seed}`,
79
186
  description: d.description,
@@ -81,9 +188,9 @@ export default mutation<SeedMarketArgs, SeedMarketResult>({
81
188
  category: d.category,
82
189
  condition: d.condition,
83
190
  status: "active",
84
- imageUrl: d.imageUrl,
191
+ imageUrl: `/images/listings/${d.photo}.webp`,
85
192
  seed: d.seed,
86
- createdAt: new Date(now - (start + n) * 7 * 60_000).toISOString(),
193
+ createdAt: new Date(now - ((start + n) * 23 + 4) * 60_000).toISOString(),
87
194
  });
88
195
  n++;
89
196
  }
@@ -0,0 +1,101 @@
1
+ import { mutation } from "@pylonsync/functions";
2
+
3
+ // Demo offers for first boot, made by the caller (the "collector" account in
4
+ // client/market.ts). Each entry is an ordinary offer the caller could send
5
+ // through makeOffer, or a purchase it could make through buyNow, so this
6
+ // grants no extra power: it skips listings the caller owns and listings that
7
+ // are no longer active. Idempotent per caller: a buyer that already has
8
+ // offers is skipped.
9
+ const PLAN: Array<{
10
+ seed: string;
11
+ amount?: number;
12
+ buyerName: string;
13
+ message?: string;
14
+ minutesAgo: number;
15
+ buy?: boolean;
16
+ }> = [
17
+ { seed: "a1f3", amount: 600, buyerName: "Leo T.", minutesAgo: 31 },
18
+ { seed: "a1f3", amount: 620, buyerName: "Ines M.", message: "Could pick up this weekend.", minutesAgo: 6 },
19
+ { seed: "b7c2", amount: 800, buyerName: "Kenji A.", minutesAgo: 14 },
20
+ { seed: "c4e9", amount: 215, buyerName: "Dana R.", message: "Does the meter need a battery?", minutesAgo: 26 },
21
+ { seed: "d8a1", amount: 380, buyerName: "Ines M.", minutesAgo: 22 },
22
+ { seed: "a9c4", amount: 260, buyerName: "Leo T.", minutesAgo: 44 },
23
+ { seed: "e2b6", buyerName: "Kenji A.", minutesAgo: 11, buy: true },
24
+ { seed: "n8w3", amount: 400, buyerName: "Dana R.", message: "I can pick up Saturday morning.", minutesAgo: 3 },
25
+ { seed: "p2d6", amount: 60, buyerName: "Ines M.", minutesAgo: 17 },
26
+ ];
27
+
28
+ interface SeedOffersResult {
29
+ seeded: number;
30
+ }
31
+
32
+ type Row = {
33
+ id: string;
34
+ seed: string;
35
+ title: string;
36
+ price: number;
37
+ sellerId: string;
38
+ status: string;
39
+ };
40
+
41
+ export default mutation<Record<string, never>, SeedOffersResult>({
42
+ args: {},
43
+ async handler(ctx) {
44
+ const buyerId = ctx.auth.userId;
45
+ if (!buyerId) throw ctx.error("UNAUTHENTICATED", "sign in first");
46
+
47
+ const existing = (await ctx.db.list("Offer")) as Array<{ buyerId: string }>;
48
+ if (existing.some((o) => o.buyerId === buyerId)) return { seeded: 0 };
49
+
50
+ const listings = (await ctx.db.list("Listing")) as Row[];
51
+ const bySeed = new Map(listings.map((l) => [l.seed, l]));
52
+ const now = Date.now();
53
+ let seeded = 0;
54
+
55
+ for (const entry of PLAN) {
56
+ const listing = bySeed.get(entry.seed);
57
+ if (!listing || listing.sellerId === buyerId || listing.status !== "active") {
58
+ continue;
59
+ }
60
+ const createdAt = new Date(now - entry.minutesAgo * 60_000).toISOString();
61
+
62
+ if (entry.buy) {
63
+ const offerId = await ctx.db.insert("Offer", {
64
+ listingId: listing.id,
65
+ listingTitle: listing.title,
66
+ sellerId: listing.sellerId,
67
+ buyerId,
68
+ buyerName: entry.buyerName,
69
+ amount: listing.price,
70
+ message: "Bought at list price",
71
+ status: "accepted",
72
+ createdAt,
73
+ });
74
+ await ctx.db.update("Listing", listing.id, { status: "sold" });
75
+ listing.status = "sold";
76
+ const siblings = (await ctx.db.query("Offer", {
77
+ listingId: listing.id,
78
+ })) as Array<{ id: string; status: string }>;
79
+ for (const o of siblings) {
80
+ if (o.id !== offerId && o.status === "pending") {
81
+ await ctx.db.update("Offer", o.id, { status: "declined" });
82
+ }
83
+ }
84
+ } else {
85
+ await ctx.db.insert("Offer", {
86
+ listingId: listing.id,
87
+ listingTitle: listing.title,
88
+ sellerId: listing.sellerId,
89
+ buyerId,
90
+ buyerName: entry.buyerName,
91
+ amount: entry.amount ?? listing.price,
92
+ message: entry.message ?? "",
93
+ status: "pending",
94
+ createdAt,
95
+ });
96
+ }
97
+ seeded++;
98
+ }
99
+ return { seeded };
100
+ },
101
+ });
@@ -1,7 +1,39 @@
1
- import type { Listing } from "../client/market";
1
+ import type { Listing, Offer } from "../client/market";
2
2
 
3
3
  export type CatalogSort = "latest" | "price-low" | "price-high";
4
4
 
5
+ export interface Category {
6
+ id: string;
7
+ label: string;
8
+ }
9
+
10
+ // Browse + sell categories. The id is what a Listing stores; the label is
11
+ // what the UI shows.
12
+ export const CATEGORIES: Category[] = [
13
+ { id: "furniture", label: "Furniture" },
14
+ { id: "lighting", label: "Lighting" },
15
+ { id: "audio", label: "Audio" },
16
+ { id: "cameras", label: "Cameras" },
17
+ { id: "watches", label: "Watches" },
18
+ { id: "clothing", label: "Clothing" },
19
+ { id: "shoes", label: "Shoes" },
20
+ { id: "bags", label: "Bags" },
21
+ { id: "bikes", label: "Bikes" },
22
+ { id: "instruments", label: "Instruments" },
23
+ { id: "kitchen", label: "Kitchen" },
24
+ { id: "other", label: "Other" },
25
+ ];
26
+
27
+ export const CONDITIONS = ["new", "like-new", "good", "fair"] as const;
28
+
29
+ export function categoryLabel(id: string): string {
30
+ return CATEGORIES.find((c) => c.id === id)?.label ?? id;
31
+ }
32
+
33
+ export function isCategory(id: string): boolean {
34
+ return CATEGORIES.some((c) => c.id === id);
35
+ }
36
+
5
37
  export function browseListings(
6
38
  listings: Listing[],
7
39
  options: { category?: string; query?: string; sort?: string },
@@ -21,6 +53,7 @@ export function browseListings(
21
53
  listing.description,
22
54
  listing.category,
23
55
  listing.sellerName,
56
+ listing.location ?? "",
24
57
  ].some((value) => value.toLocaleLowerCase().includes(query));
25
58
  });
26
59
 
@@ -30,3 +63,41 @@ export function browseListings(
30
63
  return b.createdAt.localeCompare(a.createdAt);
31
64
  });
32
65
  }
66
+
67
+ /** Pending offers per listing id. */
68
+ export function pendingOfferCounts(offers: Offer[]): Map<string, number> {
69
+ const counts = new Map<string, number>();
70
+ for (const offer of offers) {
71
+ if (offer.status !== "pending") continue;
72
+ counts.set(offer.listingId, (counts.get(offer.listingId) ?? 0) + 1);
73
+ }
74
+ return counts;
75
+ }
76
+
77
+ /** An offer as a whole-number percentage of the asking price. */
78
+ export function percentOfAsk(amount: number, price: number): number {
79
+ if (!(price > 0)) return 0;
80
+ return Math.round((amount / price) * 100);
81
+ }
82
+
83
+ /**
84
+ * Suggested offer amounts: 95%, 90%, and 85% of the asking price, rounded to
85
+ * a clean step, deduplicated, and always below the asking price.
86
+ */
87
+ export function suggestedOffers(price: number): number[] {
88
+ if (!(price > 0)) return [];
89
+ const step = price >= 500 ? 10 : price >= 100 ? 5 : 1;
90
+ const amounts = [0.95, 0.9, 0.85].map((ratio) =>
91
+ Math.max(step, Math.round((price * ratio) / step) * step),
92
+ );
93
+ return Array.from(new Set(amounts)).filter((amount) => amount < price);
94
+ }
95
+
96
+ const BUNDLED_PHOTO = /^(\/images\/listings\/[a-z0-9-]+)\.webp$/;
97
+
98
+ /** Responsive `srcset` for a bundled listing photo; undefined for others. */
99
+ export function listingSrcSet(imageUrl: string | undefined): string | undefined {
100
+ const match = imageUrl?.match(BUNDLED_PHOTO);
101
+ if (!match) return undefined;
102
+ return `${match[1]}-640.webp 640w, ${imageUrl} 1120w`;
103
+ }