@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,33 +1,14 @@
1
1
  "use client";
2
2
 
3
- import React, { useState } from "react";
3
+ import React, { useRef, useState } from "react";
4
4
  import { callFn, db } from "@pylonsync/react";
5
- import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
6
- import { Badge } from "@/components/ui/badge";
7
- import { Button } from "@/components/ui/button";
8
- import {
9
- Card,
10
- CardContent,
11
- CardHeader,
12
- CardTitle,
13
- } from "@/components/ui/card";
14
- import {
15
- Empty,
16
- EmptyDescription,
17
- EmptyHeader,
18
- EmptyTitle,
19
- } from "@/components/ui/empty";
20
- import {
21
- Field,
22
- FieldGroup,
23
- FieldLabel,
24
- FieldSeparator,
25
- } from "@/components/ui/field";
26
- import { Input } from "@/components/ui/input";
5
+ import { Check, X } from "lucide-react";
6
+ import { cn } from "@/lib/utils";
27
7
  import { Spinner } from "@/components/ui/spinner";
28
- import { Textarea } from "@/components/ui/textarea";
29
- import { AuthGate, MarketProvider, useIdentity } from "./MarketProvider";
30
- import { money, timeAgo, type Offer } from "./market";
8
+ import { percentOfAsk, suggestedOffers } from "../lib/catalog";
9
+ import { LoginCard } from "./LoginCard";
10
+ import { MarketProvider, useIdentity } from "./MarketProvider";
11
+ import { avatarColor, initials, money, timeAgo, type Offer } from "./market";
31
12
 
32
13
  interface Props {
33
14
  listingId: string;
@@ -39,74 +20,151 @@ interface Props {
39
20
  initialOffers?: Offer[];
40
21
  }
41
22
 
42
- type BadgeVariant = "default" | "secondary" | "destructive" | "outline" | "success" | "warning";
43
-
44
- const statusVariant: Record<string, BadgeVariant> = {
45
- pending: "warning",
46
- accepted: "success",
47
- declined: "outline",
48
- };
49
-
23
+ // Buy box + live offers for one listing. One live query (every offer on this
24
+ // listing) drives all of it: the buyer's own offer status, the public offer
25
+ // list, and the seller's accept/decline queue. An offer sent from any tab
26
+ // shows up here without a refresh.
50
27
  function Panel(props: Props) {
51
- const { listingId, sellerId, price } = props;
28
+ const { listingId, sellerId } = props;
52
29
  const identity = useIdentity();
53
30
  const isSeller = !!identity && identity.userId === sellerId;
54
31
 
55
- // The live query — every offer on this listing, newest first. Reads are
56
- // public, so this runs for signed-out visitors too; it just lights up the
57
- // moment a buyer in another tab makes an offer.
58
- const { data } = db.useQuery<Offer>("Offer", {
32
+ const { data, loading } = db.useQuery<Offer>("Offer", {
33
+ where: { listingId },
59
34
  orderBy: { createdAt: "desc" },
60
35
  });
61
- const offers = Array.from(
62
- new Map(
63
- [...(props.initialOffers ?? []), ...(data ?? [])].map((offer) => [
64
- offer.id,
65
- offer,
66
- ]),
67
- ).values(),
68
- ).filter((offer) => offer.listingId === listingId);
36
+ const offers =
37
+ loading && (data ?? []).length === 0
38
+ ? [...(props.initialOffers ?? [])].sort((a, b) =>
39
+ b.createdAt.localeCompare(a.createdAt),
40
+ )
41
+ : (data ?? []);
69
42
  const isSold =
70
43
  props.status === "sold" || offers.some((offer) => offer.status === "accepted");
44
+
45
+ if (isSeller) {
46
+ return <SellerView offers={offers} price={props.price} />;
47
+ }
48
+
71
49
  const myOffer = identity
72
50
  ? offers.find((o) => o.buyerId === identity.userId)
73
51
  : undefined;
74
52
 
75
- if (isSeller) {
76
- return <SellerView offers={offers} />;
77
- }
78
- // Making an offer needs a real account — gate it (prefilled demo login).
79
53
  return (
80
- <AuthGate
81
- title="Sign in to make an offer"
82
- blurb="Offers are tied to a real account so the seller knows who is bidding. The demo account is ready; just select Log in."
54
+ <div className="flex flex-col gap-8">
55
+ <BuyBox {...props} myOffer={myOffer} isSold={isSold} />
56
+ <OfferHistory offers={offers} price={props.price} />
57
+ </div>
58
+ );
59
+ }
60
+
61
+ // Offer keys seen on the first settled render; later keys arrived live.
62
+ function useArrivals(offers: Offer[]): (id: string) => boolean {
63
+ const seen = useRef<Set<string> | null>(null);
64
+ if (seen.current === null) seen.current = new Set(offers.map((o) => o.id));
65
+ return (id) => !seen.current!.has(id);
66
+ }
67
+
68
+ function StatusTag({ status }: { status: Offer["status"] }) {
69
+ return (
70
+ <span
71
+ className={cn(
72
+ "rounded-full px-2 py-0.5 text-[11px] font-semibold capitalize",
73
+ status === "pending" && "bg-signal/12 text-signal-foreground",
74
+ status === "accepted" && "bg-success/14 text-success-foreground",
75
+ status === "declined" && "bg-muted text-muted-foreground",
76
+ )}
83
77
  >
84
- <BuyerView
85
- {...props}
86
- myOffer={myOffer}
87
- isSold={isSold}
88
- suggestedPrice={price}
89
- />
90
- </AuthGate>
78
+ {status}
79
+ </span>
80
+ );
81
+ }
82
+
83
+ function Avatar({ name, className }: { name: string; className?: string }) {
84
+ return (
85
+ <span
86
+ aria-hidden="true"
87
+ className={cn(
88
+ "grid size-9 shrink-0 place-items-center rounded-full text-xs font-semibold text-white",
89
+ className,
90
+ )}
91
+ style={{ background: avatarColor(name) }}
92
+ >
93
+ {initials(name)}
94
+ </span>
95
+ );
96
+ }
97
+
98
+ function OfferHistory({ offers, price }: { offers: Offer[]; price: number }) {
99
+ const isNew = useArrivals(offers);
100
+ const pending = offers.filter((o) => o.status === "pending").length;
101
+
102
+ return (
103
+ <section aria-labelledby="offers-heading">
104
+ <div className="flex items-baseline justify-between">
105
+ <h2 id="offers-heading" className="text-base font-semibold">
106
+ Offers
107
+ </h2>
108
+ <p className="text-[13px] text-muted-foreground">
109
+ {pending} open · {offers.length} total
110
+ </p>
111
+ </div>
112
+ {offers.length === 0 ? (
113
+ <p className="mt-3 rounded-2xl bg-muted/60 px-4 py-5 text-sm text-muted-foreground">
114
+ No offers yet. New offers appear here as buyers send them.
115
+ </p>
116
+ ) : (
117
+ <ol aria-live="polite" className="mt-3 flex flex-col">
118
+ {offers.slice(0, 6).map((o) => (
119
+ <li
120
+ key={o.id}
121
+ className={cn(
122
+ "flex items-center gap-3 rounded-xl border-b border-border/60 px-2 py-2.5 last:border-b-0",
123
+ isNew(o.id) && "market-rise market-flash",
124
+ )}
125
+ >
126
+ <Avatar name={o.buyerName} />
127
+ <div className="min-w-0 flex-1">
128
+ <p className="truncate text-sm">
129
+ <span className="font-medium">{o.buyerName}</span>{" "}
130
+ <span className="text-muted-foreground">
131
+ {o.message === "Bought at list price" ? "bought it" : "offered"}
132
+ </span>
133
+ </p>
134
+ <p className="text-xs text-muted-foreground">{timeAgo(o.createdAt)}</p>
135
+ </div>
136
+ <div className="text-right">
137
+ <p className="text-sm font-semibold tabular-nums">{money(o.amount)}</p>
138
+ <p className="text-[11px] tabular-nums text-muted-foreground">
139
+ {percentOfAsk(o.amount, price)}% of ask
140
+ </p>
141
+ </div>
142
+ <StatusTag status={o.status} />
143
+ </li>
144
+ ))}
145
+ </ol>
146
+ )}
147
+ </section>
91
148
  );
92
149
  }
93
150
 
94
- function SellerView({ offers }: { offers: Offer[] }) {
151
+ function SellerView({ offers, price }: { offers: Offer[]; price: number }) {
95
152
  const [busy, setBusy] = useState<string | null>(null);
96
153
  const [confirming, setConfirming] = useState<string | null>(null);
97
154
  const [err, setErr] = useState<string | null>(null);
155
+ const isNew = useArrivals(offers);
98
156
  const pending = offers.filter((o) => o.status === "pending");
99
157
 
100
158
  async function respond(offerId: string, accept: boolean) {
101
159
  setBusy(offerId);
102
160
  setErr(null);
103
161
  try {
104
- // Use the direct function transport for seller decisions. It remains
105
- // reliable across dev-server reconnects, while the live query applies
106
- // the atomic offer + listing updates as soon as the server broadcasts.
162
+ // The direct function call stays reliable across dev-server
163
+ // reconnects; the live query applies the offer + listing updates when
164
+ // the server broadcasts them.
107
165
  await callFn("respondToOffer", { offerId, accept });
108
166
  } catch (e) {
109
- setErr((e as Error).message ?? "Could not respond to offer.");
167
+ setErr((e as Error).message ?? "Could not answer the offer.");
110
168
  } finally {
111
169
  setBusy(null);
112
170
  setConfirming(null);
@@ -114,45 +172,50 @@ function SellerView({ offers }: { offers: Offer[] }) {
114
172
  }
115
173
 
116
174
  return (
117
- <div className="flex flex-col gap-3">
118
- <div className="flex items-center justify-between">
119
- <h2 className="font-semibold">Offers on your listing</h2>
120
- <Badge variant="outline">{pending.length} pending</Badge>
175
+ <section
176
+ aria-labelledby="seller-offers-heading"
177
+ className="rounded-2xl bg-card p-4 shadow-[var(--shadow-border)] sm:p-5"
178
+ >
179
+ <div className="flex items-baseline justify-between">
180
+ <h2 id="seller-offers-heading" className="text-base font-semibold">
181
+ Offers on your listing
182
+ </h2>
183
+ <span className="text-[13px] text-muted-foreground">
184
+ {pending.length} waiting
185
+ </span>
121
186
  </div>
122
187
  <div aria-live="polite">
123
188
  {err ? (
124
- <Alert variant="destructive">
125
- <AlertDescription>{err}</AlertDescription>
126
- </Alert>
189
+ <p className="mt-3 rounded-xl bg-destructive/10 px-3 py-2 text-sm text-destructive">
190
+ {err}
191
+ </p>
127
192
  ) : null}
128
193
  </div>
129
194
  {offers.length === 0 ? (
130
- <Empty className="border-0 bg-muted py-6">
131
- <EmptyHeader>
132
- <EmptyTitle className="text-base">No offers yet</EmptyTitle>
133
- <EmptyDescription>
134
- They will appear here as soon as a buyer sends one.
135
- </EmptyDescription>
136
- </EmptyHeader>
137
- </Empty>
195
+ <p className="mt-3 text-sm text-muted-foreground">
196
+ No offers yet. They appear here the moment a buyer sends one.
197
+ </p>
138
198
  ) : (
139
- <ul className="flex flex-col gap-2">
199
+ <ul className="mt-3 flex flex-col gap-2">
140
200
  {offers.map((o) => (
141
- <li key={o.id}>
142
- <Card className="flex items-center justify-between gap-3 rounded-lg p-3">
143
- <div className="min-w-0">
144
- <div className="flex items-center gap-2">
201
+ <li
202
+ key={o.id}
203
+ className={cn(
204
+ "flex flex-wrap items-center gap-3 rounded-xl bg-background/60 p-3 ring-1 ring-border/60",
205
+ isNew(o.id) && "market-rise market-flash",
206
+ )}
207
+ >
208
+ <Avatar name={o.buyerName} />
209
+ <div className="min-w-0 flex-1">
210
+ <p className="flex items-center gap-2">
145
211
  <span className="text-lg font-semibold tabular-nums">
146
212
  {money(o.amount)}
147
213
  </span>
148
- <Badge
149
- variant={statusVariant[o.status] ?? "outline"}
150
- className="capitalize"
151
- >
152
- {o.status}
153
- </Badge>
154
- </div>
155
- <p className="truncate text-sm text-muted-foreground">
214
+ <span className="text-xs tabular-nums text-muted-foreground">
215
+ {percentOfAsk(o.amount, price)}% of ask
216
+ </span>
217
+ </p>
218
+ <p className="truncate text-[13px] text-muted-foreground">
156
219
  {o.buyerName} · {timeAgo(o.createdAt)}
157
220
  {o.message ? ` · “${o.message}”` : ""}
158
221
  </p>
@@ -161,76 +224,80 @@ function SellerView({ offers }: { offers: Offer[] }) {
161
224
  <div className="flex shrink-0 gap-2">
162
225
  {confirming === o.id ? (
163
226
  <>
164
- <Button
165
- size="sm"
227
+ <button
228
+ type="button"
166
229
  disabled={busy === o.id}
167
230
  onClick={() => respond(o.id, true)}
231
+ className="inline-flex min-h-9 items-center gap-1.5 rounded-full bg-primary px-3.5 text-[13px] font-medium text-primary-foreground disabled:opacity-60"
168
232
  >
169
- Confirm
170
- </Button>
171
- <Button
172
- size="sm"
173
- variant="ghost"
233
+ {busy === o.id ? <Spinner /> : <Check className="size-3.5" />}
234
+ Sell for {money(o.amount)}
235
+ </button>
236
+ <button
237
+ type="button"
174
238
  disabled={busy === o.id}
175
239
  onClick={() => setConfirming(null)}
240
+ className="inline-flex min-h-9 items-center rounded-full px-3 text-[13px] font-medium text-muted-foreground hover:text-foreground"
176
241
  >
177
242
  Cancel
178
- </Button>
243
+ </button>
179
244
  </>
180
245
  ) : (
181
- <Button
182
- size="sm"
183
- disabled={busy === o.id}
184
- onClick={() => setConfirming(o.id)}
185
- >
186
- Accept
187
- </Button>
246
+ <>
247
+ <button
248
+ type="button"
249
+ disabled={busy === o.id}
250
+ onClick={() => setConfirming(o.id)}
251
+ className="inline-flex min-h-9 items-center rounded-full bg-primary px-3.5 text-[13px] font-medium text-primary-foreground transition-[scale] active:scale-[0.97] disabled:opacity-60"
252
+ >
253
+ Accept
254
+ </button>
255
+ <button
256
+ type="button"
257
+ disabled={busy === o.id}
258
+ onClick={() => respond(o.id, false)}
259
+ aria-label={`Decline ${money(o.amount)} offer from ${o.buyerName}`}
260
+ className="inline-flex min-h-9 items-center gap-1 rounded-full bg-card px-3 text-[13px] font-medium shadow-[var(--shadow-border)] transition-[scale] active:scale-[0.97] disabled:opacity-60"
261
+ >
262
+ <X className="size-3.5" />
263
+ Decline
264
+ </button>
265
+ </>
188
266
  )}
189
- {confirming !== o.id ? (
190
- <Button
191
- size="sm"
192
- variant="outline"
193
- disabled={busy === o.id}
194
- onClick={() => respond(o.id, false)}
195
- >
196
- Decline
197
- </Button>
198
- ) : null}
199
267
  </div>
200
- ) : null}
201
- </Card>
268
+ ) : (
269
+ <StatusTag status={o.status} />
270
+ )}
202
271
  </li>
203
272
  ))}
204
273
  </ul>
205
274
  )}
206
- </div>
275
+ </section>
207
276
  );
208
277
  }
209
278
 
210
- function BuyerView({
279
+ function BuyBox({
211
280
  listingId,
212
281
  title,
213
282
  sellerId,
214
283
  sellerName,
284
+ price,
215
285
  myOffer,
216
286
  isSold,
217
- suggestedPrice,
218
- }: Props & { myOffer?: Offer; isSold: boolean; suggestedPrice: number }) {
219
- // Rendered inside <AuthGate>, so identity is non-null here.
287
+ }: Props & { myOffer?: Offer; isSold: boolean }) {
220
288
  const identity = useIdentity();
221
289
  const userId = identity?.userId ?? "";
222
290
  const name = identity?.name ?? "you";
223
- const [amount, setAmount] = useState(String(suggestedPrice));
291
+ const [mode, setMode] = useState<"idle" | "offer" | "buy" | "signin">("idle");
292
+ const [amount, setAmount] = useState(String(suggestedOffers(price)[1] ?? price));
224
293
  const [message, setMessage] = useState("");
225
- const [confirmingBuy, setConfirmingBuy] = useState(false);
226
294
  const [err, setErr] = useState<string | null>(null);
227
295
 
228
- // Local-first optimism, baked in: db.useMutation paints the Offer into the
229
- // local store the instant you click (the `optimistic` ghost), so the live
230
- // query below renders "Your offer" immediately — no waiting on the server,
231
- // no hand-rolled state. The server's makeOffer reuses the same id (threaded
232
- // as _optimisticId), so its broadcast merges in place; on failure the engine
233
- // rolls the ghost back on its own.
296
+ // db.useMutation paints the Offer into the local store the moment the
297
+ // buyer submits (the `optimistic` row), so "Your offer" renders before the
298
+ // server answers. makeOffer reuses the same id (threaded as
299
+ // _optimisticId), so the server row replaces it in place; on failure the
300
+ // engine rolls it back.
234
301
  const makeOffer = db.useMutation<
235
302
  { listingId: string; amount: number; message: string; buyerName: string },
236
303
  { id: string }
@@ -252,9 +319,8 @@ function BuyerView({
252
319
  }),
253
320
  });
254
321
 
255
- // Buy now: same optimistic pattern, but the ghost is an *accepted* offer at
256
- // the list price — the buyer sees "🎉 Accepted" instantly while the server
257
- // marks the listing sold + declines other bids.
322
+ // Buy now: the optimistic row is an accepted offer at the asking price;
323
+ // the server marks the listing sold and declines the other offers.
258
324
  const buyNow = db.useMutation<{ listingId: string; buyerName: string }, { id: string }>(
259
325
  "buyNow",
260
326
  {
@@ -267,7 +333,7 @@ function BuyerView({
267
333
  sellerId,
268
334
  buyerId: userId,
269
335
  buyerName: name,
270
- amount: suggestedPrice,
336
+ amount: price,
271
337
  message: "Bought at list price",
272
338
  status: "accepted",
273
339
  createdAt: ctx.now,
@@ -276,6 +342,11 @@ function BuyerView({
276
342
  },
277
343
  );
278
344
 
345
+ function start(next: "offer" | "buy") {
346
+ setErr(null);
347
+ setMode(identity ? next : "signin");
348
+ }
349
+
279
350
  async function buy() {
280
351
  setErr(null);
281
352
  try {
@@ -285,49 +356,6 @@ function BuyerView({
285
356
  }
286
357
  }
287
358
 
288
- // `myOffer` now includes the optimistic ghost, so this flips the instant
289
- // the offer is made.
290
- if (myOffer) {
291
- return (
292
- <Card>
293
- <CardHeader className="pb-3">
294
- <CardTitle>Your offer</CardTitle>
295
- </CardHeader>
296
- <CardContent className="flex flex-col gap-2">
297
- <div className="flex items-center gap-2">
298
- <span className="text-2xl font-semibold tabular-nums">
299
- {money(myOffer.amount)}
300
- </span>
301
- <Badge
302
- variant={statusVariant[myOffer.status] ?? "outline"}
303
- className="capitalize"
304
- >
305
- {myOffer.status}
306
- </Badge>
307
- </div>
308
- <p className="text-sm text-muted-foreground">
309
- {myOffer.status === "pending"
310
- ? `Sent to ${sellerName}. Their answer will appear here live.`
311
- : myOffer.status === "accepted"
312
- ? "Accepted. Confirm payment and delivery with the seller."
313
- : "This offer was declined."}
314
- </p>
315
- </CardContent>
316
- </Card>
317
- );
318
- }
319
-
320
- if (isSold) {
321
- return (
322
- <Alert>
323
- <AlertTitle>This item has sold</AlertTitle>
324
- <AlertDescription>
325
- Browse other finds to discover something similar.
326
- </AlertDescription>
327
- </Alert>
328
- );
329
- }
330
-
331
359
  async function submit(e: React.FormEvent) {
332
360
  e.preventDefault();
333
361
  const value = Number.parseFloat(amount);
@@ -337,129 +365,235 @@ function BuyerView({
337
365
  }
338
366
  setErr(null);
339
367
  try {
340
- // The ghost is painted synchronously here; the view has already flipped
341
- // to "Your offer" by the time this awaits.
342
368
  await makeOffer.mutate({ listingId, amount: value, message, buyerName: name });
369
+ setMode("idle");
343
370
  } catch (e) {
344
- setErr((e as Error).message ?? "Could not send offer.");
371
+ setErr((e as Error).message ?? "Could not send the offer.");
345
372
  }
346
373
  }
347
374
 
375
+ if (myOffer && !(myOffer.status === "declined" && mode === "offer")) {
376
+ const bought = myOffer.message === "Bought at list price";
377
+ return (
378
+ <div className="rounded-2xl bg-card p-5 shadow-[var(--shadow-border)]">
379
+ <div className="flex items-center justify-between gap-3">
380
+ <p className="text-sm font-medium">
381
+ {bought ? "You bought this item" : "Your offer"}
382
+ </p>
383
+ <StatusTag status={myOffer.status} />
384
+ </div>
385
+ <p className="mt-2 text-[32px] font-semibold leading-none tabular-nums tracking-[-0.02em]">
386
+ {money(myOffer.amount)}
387
+ </p>
388
+ <p className="mt-3 text-sm text-muted-foreground">
389
+ {myOffer.status === "pending"
390
+ ? `Sent to ${sellerName}. Their answer appears here as soon as they respond.`
391
+ : myOffer.status === "accepted"
392
+ ? `${bought ? "Purchase confirmed." : `${sellerName} accepted.`} Arrange payment and pickup with the seller.`
393
+ : `${sellerName} declined this offer.`}
394
+ </p>
395
+ {myOffer.status === "declined" && !isSold ? (
396
+ <button
397
+ type="button"
398
+ onClick={() => setMode("offer")}
399
+ className="mt-4 inline-flex min-h-10 items-center rounded-full bg-card px-4 text-sm font-medium shadow-[var(--shadow-border)]"
400
+ >
401
+ Make a new offer
402
+ </button>
403
+ ) : null}
404
+ </div>
405
+ );
406
+ }
407
+
408
+ if (isSold) {
409
+ return (
410
+ <div className="rounded-2xl bg-card p-5 shadow-[var(--shadow-border)]">
411
+ <p className="font-display text-2xl">This item sold</p>
412
+ <p className="mt-1 text-sm text-muted-foreground">
413
+ The seller accepted an offer. Other listings are on the home page.
414
+ </p>
415
+ </div>
416
+ );
417
+ }
418
+
419
+ if (mode === "signin") {
420
+ return (
421
+ <LoginCard
422
+ title="Sign in to buy or make an offer"
423
+ blurb="The demo account is filled in. Select Log in to continue."
424
+ onDone={() => setMode("idle")}
425
+ className="max-w-none"
426
+ />
427
+ );
428
+ }
429
+
430
+ const suggestions = suggestedOffers(price);
431
+ const offerValue = Number.parseFloat(amount) || 0;
432
+
348
433
  return (
349
- <Card>
350
- <CardContent className="flex flex-col gap-4 p-5">
351
- <div className="flex flex-col gap-2">
352
- {confirmingBuy ? (
353
- <Alert>
354
- <AlertTitle>
355
- Buy this item for {money(suggestedPrice)}?
356
- </AlertTitle>
357
- <AlertDescription>
358
- This accepts the asking price and marks the listing sold.
359
- </AlertDescription>
360
- <div className="mt-3 flex gap-2">
361
- <Button
362
- type="button"
363
- size="sm"
364
- onClick={buy}
365
- disabled={buyNow.loading}
366
- className="flex-1"
367
- >
368
- {buyNow.loading ? <Spinner data-icon="inline-start" /> : null}
369
- {buyNow.loading ? "Buying…" : "Confirm purchase"}
370
- </Button>
371
- <Button
372
- type="button"
373
- size="sm"
374
- variant="outline"
375
- onClick={() => setConfirmingBuy(false)}
376
- disabled={buyNow.loading}
377
- >
378
- Cancel
379
- </Button>
380
- </div>
381
- </Alert>
382
- ) : (
383
- <>
384
- <Button
434
+ <div className="rounded-2xl bg-card p-4 shadow-[var(--shadow-border)] sm:p-5">
435
+ {mode === "buy" ? (
436
+ <div className="market-rise">
437
+ <p className="text-sm font-medium">Buy {title} for {money(price)}?</p>
438
+ <p className="mt-1 text-sm text-muted-foreground">
439
+ This pays the asking price and marks the listing sold.
440
+ </p>
441
+ <div className="mt-4 flex gap-2">
442
+ <button
385
443
  type="button"
386
- onClick={() => setConfirmingBuy(true)}
387
- className="w-full"
444
+ onClick={buy}
445
+ disabled={buyNow.loading}
446
+ className="inline-flex min-h-12 flex-1 items-center justify-center gap-2 rounded-full bg-primary px-5 text-[15px] font-medium text-primary-foreground transition-[scale] active:scale-[0.98] disabled:opacity-60"
388
447
  >
389
- Buy now for {money(suggestedPrice)}
390
- </Button>
391
- <p className="text-center text-xs text-muted-foreground">
392
- Instant purchase at the asking price.
393
- </p>
394
- </>
395
- )}
396
- </div>
397
-
398
- <FieldSeparator>or make an offer</FieldSeparator>
448
+ {buyNow.loading ? <Spinner /> : null}
449
+ {buyNow.loading ? "Buying" : `Confirm purchase`}
450
+ </button>
451
+ <button
452
+ type="button"
453
+ onClick={() => setMode("idle")}
454
+ disabled={buyNow.loading}
455
+ className="inline-flex min-h-12 items-center rounded-full px-5 text-sm font-medium text-muted-foreground hover:text-foreground"
456
+ >
457
+ Cancel
458
+ </button>
459
+ </div>
460
+ </div>
461
+ ) : (
462
+ <div className="flex flex-col gap-2 sm:flex-row">
463
+ <button
464
+ type="button"
465
+ onClick={() => start("buy")}
466
+ className="inline-flex min-h-12 flex-1 items-center justify-center rounded-full bg-primary px-5 text-[15px] font-medium text-primary-foreground transition-[background-color,scale] duration-150 hover:bg-primary/88 active:scale-[0.98]"
467
+ >
468
+ Buy now
469
+ </button>
470
+ <button
471
+ type="button"
472
+ onClick={() => start("offer")}
473
+ aria-expanded={mode === "offer"}
474
+ className={cn(
475
+ "inline-flex min-h-12 flex-1 items-center justify-center rounded-full px-5 text-[15px] font-medium transition-[background-color,box-shadow,scale] duration-150 active:scale-[0.98]",
476
+ mode === "offer"
477
+ ? "bg-muted"
478
+ : "bg-card shadow-[var(--shadow-border)] hover:shadow-[var(--shadow-border-hover)]",
479
+ )}
480
+ >
481
+ Make an offer
482
+ </button>
483
+ </div>
484
+ )}
399
485
 
400
- <form onSubmit={submit}>
401
- <FieldGroup className="gap-3">
402
- <Field>
403
- <FieldLabel htmlFor="offer-amount">Your offer ($)</FieldLabel>
404
- <Input
405
- id="offer-amount"
406
- name="offerAmount"
407
- type="number"
408
- inputMode="decimal"
486
+ {mode === "offer" ? (
487
+ <form onSubmit={submit} className="market-rise mt-5 flex flex-col gap-4">
488
+ <div>
489
+ <label htmlFor="offer-amount" className="text-sm font-medium">
490
+ Your offer
491
+ </label>
492
+ <div className="mt-2 flex items-center rounded-xl bg-background px-4 ring-1 ring-input focus-within:ring-2 focus-within:ring-ring">
493
+ <span className="text-2xl font-semibold text-muted-foreground">$</span>
494
+ <input
495
+ id="offer-amount"
496
+ name="offerAmount"
497
+ type="number"
498
+ inputMode="decimal"
499
+ autoComplete="off"
500
+ min="1"
501
+ step="1"
502
+ value={amount}
503
+ onChange={(e) => setAmount(e.target.value)}
504
+ className="h-14 w-full bg-transparent pl-1 text-2xl font-semibold tabular-nums outline-none focus-visible:outline-none"
505
+ />
506
+ <span className="shrink-0 text-xs tabular-nums text-muted-foreground">
507
+ {percentOfAsk(offerValue, price)}% of {money(price)}
508
+ </span>
509
+ </div>
510
+ {suggestions.length > 0 ? (
511
+ <div className="mt-2 flex flex-wrap gap-1.5">
512
+ {suggestions.map((value) => (
513
+ <button
514
+ key={value}
515
+ type="button"
516
+ onClick={() => setAmount(String(value))}
517
+ aria-pressed={offerValue === value}
518
+ className={cn(
519
+ "inline-flex min-h-8 items-center rounded-full px-3 text-[13px] font-medium tabular-nums transition-colors",
520
+ offerValue === value
521
+ ? "bg-foreground text-background"
522
+ : "bg-muted text-foreground hover:bg-accent",
523
+ )}
524
+ >
525
+ {money(value)}
526
+ </button>
527
+ ))}
528
+ </div>
529
+ ) : null}
530
+ </div>
531
+ <div>
532
+ <label htmlFor="offer-note" className="text-sm font-medium">
533
+ Note to {sellerName}{" "}
534
+ <span className="font-normal text-muted-foreground">(optional)</span>
535
+ </label>
536
+ <textarea
537
+ id="offer-note"
538
+ name="offerNote"
409
539
  autoComplete="off"
410
- min="1"
411
- step="1"
412
- value={amount}
413
- onChange={(e) => setAmount(e.target.value)}
414
- className="max-w-32"
540
+ rows={2}
541
+ value={message}
542
+ onChange={(e) => setMessage(e.target.value)}
543
+ placeholder="Pickup time, questions about condition"
544
+ className="mt-2 w-full resize-none rounded-xl bg-background px-4 py-3 text-sm ring-1 ring-input outline-none placeholder:text-muted-foreground focus:ring-2 focus:ring-ring focus-visible:outline-none"
415
545
  />
416
- </Field>
417
- <Field>
418
- <FieldLabel htmlFor="offer-note">
419
- Note <span className="font-normal text-muted-foreground">(optional)</span>
420
- </FieldLabel>
421
- <Textarea
422
- id="offer-note"
423
- name="offerNote"
424
- autoComplete="off"
425
- placeholder="Share any useful details…"
426
- value={message}
427
- onChange={(e) => setMessage(e.target.value)}
428
- rows={2}
429
- />
430
- </Field>
431
- <div aria-live="polite">
432
- {err ? (
433
- <Alert variant="destructive">
434
- <AlertDescription>{err}</AlertDescription>
435
- </Alert>
436
- ) : null}
437
- </div>
438
- <Button
439
- type="submit"
440
- variant="outline"
441
- disabled={makeOffer.loading}
442
- className="w-full"
443
- >
444
- {makeOffer.loading ? <Spinner data-icon="inline-start" /> : null}
445
- {makeOffer.loading
446
- ? "Sending…"
447
- : `Offer ${money(Number.parseFloat(amount) || 0)}`}
448
- </Button>
449
- <p className="text-center text-xs text-muted-foreground">
450
- You're bidding as <span className="font-medium">{name}</span>
451
- </p>
452
- </FieldGroup>
453
- </form>
454
- </CardContent>
455
- </Card>
546
+ </div>
547
+ <button
548
+ type="submit"
549
+ disabled={makeOffer.loading}
550
+ className="inline-flex min-h-12 items-center justify-center gap-2 rounded-full bg-signal px-5 text-[15px] font-medium text-white transition-[filter,scale] duration-150 hover:brightness-95 active:scale-[0.98] disabled:opacity-60"
551
+ >
552
+ {makeOffer.loading ? <Spinner /> : null}
553
+ {makeOffer.loading ? "Sending" : `Send offer of ${money(offerValue)}`}
554
+ </button>
555
+ <p className="text-center text-xs text-muted-foreground">
556
+ Sending as <span className="font-medium text-foreground">{name}</span>.
557
+ The seller can accept or decline.
558
+ </p>
559
+ </form>
560
+ ) : null}
561
+
562
+ <div aria-live="polite">
563
+ {err ? (
564
+ <p className="mt-3 rounded-xl bg-destructive/10 px-3 py-2 text-sm text-destructive">
565
+ {err}
566
+ </p>
567
+ ) : null}
568
+ </div>
569
+ </div>
456
570
  );
457
571
  }
458
572
 
459
573
  export function OfferPanel(props: Props) {
460
574
  return (
461
- <MarketProvider>
575
+ <MarketProvider
576
+ fallback={
577
+ <div className="flex flex-col gap-8">
578
+ <div className="rounded-2xl bg-card p-4 shadow-[var(--shadow-border)] sm:p-5">
579
+ <div className="flex flex-col gap-2 sm:flex-row">
580
+ <div className="h-12 flex-1 rounded-full bg-muted" />
581
+ <div className="h-12 flex-1 rounded-full bg-muted" />
582
+ </div>
583
+ </div>
584
+ <StaticHistory {...props} />
585
+ </div>
586
+ }
587
+ >
462
588
  <Panel {...props} />
463
589
  </MarketProvider>
464
590
  );
465
591
  }
592
+
593
+ // Server-rendered offer list shown until the sync engine connects.
594
+ function StaticHistory({ initialOffers = [], price }: Props) {
595
+ const offers = [...initialOffers].sort((a, b) =>
596
+ b.createdAt.localeCompare(a.createdAt),
597
+ );
598
+ return <OfferHistory offers={offers} price={price} />;
599
+ }