@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
@@ -8,9 +8,10 @@
8
8
  * - The browse grid (`/`) and each listing page (`/listing/:id`) are
9
9
  * SERVER-RENDERED with real rows from the database (good for SEO + LCP) —
10
10
  * view source and the products are in the HTML, not fetched later.
11
- * - The interactive, realtime bits — the "just listed" ticker, the live
12
- * offers on a listing, your inbox on `/me` — ride the sync engine: a
13
- * single `useQuery` fans every write out to every open tab instantly.
11
+ * - The realtime parts ride the sync engine: the browse grid (new
12
+ * listings, offer counts, sold status), the activity feed, the offers on
13
+ * a listing, and your dashboard on `/me`. One `useQuery` per view fans
14
+ * every write out to every open tab.
14
15
  * - One binary, one port. SSR + REST + WebSockets all from `pylon dev`.
15
16
  * No Next.js app, no separate realtime service.
16
17
  */
@@ -21,6 +22,7 @@ import {
21
22
  buildManifest,
22
23
  discoverAppRoutes,
23
24
  discoverFunctions,
25
+ font,
24
26
  } from "@pylonsync/sdk";
25
27
 
26
28
  // Accounts. Email/password auth is built in: registering through
@@ -41,13 +43,13 @@ const User = entity(
41
43
  },
42
44
  );
43
45
 
44
- // A thing for sale. `imageUrl` is optional so listings created before the
45
- // photo field was added still render with a deterministic fallback.
46
+ // A thing for sale. A listing without `imageUrl` renders a deterministic
47
+ // gradient placeholder.
46
48
  // `status` flips active → sold when an offer is accepted.
47
49
  //
48
50
  // `sellerId: field.owner()` is what lets SellForm create a listing with a
49
- // plain, optimistic `db.insert` (it shows in the live ticker the instant
50
- // you post — no server round-trip) while the seller id stays unspoofable:
51
+ // plain, optimistic `db.insert` (it shows in every open browse grid the
52
+ // instant you post, before the server round-trip) while the seller id stays unspoofable:
51
53
  // the framework stamps it from the session and rejects any forged value.
52
54
  // No createListing function needed. `status` + `createdAt` default
53
55
  // server-side so the client doesn't have to send them.
@@ -56,6 +58,8 @@ const Listing = entity(
56
58
  {
57
59
  sellerId: field.string().owner(),
58
60
  sellerName: field.string(),
61
+ // Where the item is, as the seller wrote it ("Brooklyn, NY").
62
+ location: field.string().optional(),
59
63
  title: field.string(),
60
64
  // Human-readable URL key: "herman-miller-aeron-size-b-a1f3". Unique so it
61
65
  // addresses exactly one listing; the detail route resolves by it.
@@ -190,6 +194,27 @@ const manifest = buildManifest({
190
194
  // File-based SSR routing: app/**/page.tsx. One binary serves the frontend
191
195
  // and the API on one port.
192
196
  routes: await discoverAppRoutes(),
197
+ // Self-hosted web fonts. globals.css maps them onto Tailwind's font-sans
198
+ // and font-display.
199
+ fonts: [
200
+ font({
201
+ family: "Geist",
202
+ variable: "--font-geist",
203
+ weights: ["400", "500", "600"],
204
+ subsets: ["latin"],
205
+ display: "swap",
206
+ preload: true,
207
+ }),
208
+ font({
209
+ family: "Instrument Serif",
210
+ variable: "--font-instrument",
211
+ weights: ["400"],
212
+ styles: ["normal", "italic"],
213
+ subsets: ["latin"],
214
+ display: "swap",
215
+ preload: true,
216
+ }),
217
+ ],
193
218
  });
194
219
 
195
220
  // Not a debug leftover: the CLI runs `bun run app.ts` and parses stdout as
@@ -0,0 +1,311 @@
1
+ "use client";
2
+
3
+ import React, { useRef } from "react";
4
+ import { Link, db } from "@pylonsync/react";
5
+ import { cn } from "@/lib/utils";
6
+ import { listingSrcSet } from "../lib/catalog";
7
+ import { MarketProvider } from "./MarketProvider";
8
+ import { gradient, money, timeAgo, type Listing, type Offer } from "./market";
9
+
10
+ // Live marketplace activity: new listings, offers, and sales, merged from two
11
+ // live queries. A row written in any tab (by any visitor) slides in at the
12
+ // top with a short highlight. Reads are public, so this runs for signed-out
13
+ // visitors on the guest session.
14
+
15
+ type ActivityEvent = {
16
+ key: string;
17
+ kind: "listed" | "offer" | "sold";
18
+ at: string;
19
+ listing?: Listing;
20
+ title: string;
21
+ href: string;
22
+ who: string;
23
+ amount: number;
24
+ };
25
+
26
+ function useEvents(limit: number): {
27
+ events: ActivityEvent[];
28
+ forSale: number;
29
+ openOffers: number;
30
+ loading: boolean;
31
+ } {
32
+ const { data: listingRows, loading } = db.useQuery<Listing>("Listing", {
33
+ orderBy: { createdAt: "desc" },
34
+ });
35
+ const { data: offerRows } = db.useQuery<Offer>("Offer", {
36
+ orderBy: { createdAt: "desc" },
37
+ limit: 40,
38
+ });
39
+ const listings = listingRows ?? [];
40
+ const offers = offerRows ?? [];
41
+ const byId = new Map(listings.map((l) => [l.id, l]));
42
+
43
+ const events: ActivityEvent[] = [
44
+ ...listings.slice(0, 20).map((l) => ({
45
+ key: `l-${l.id}`,
46
+ kind: "listed" as const,
47
+ at: l.createdAt,
48
+ listing: l,
49
+ title: l.title,
50
+ href: `/listing/${l.slug || l.id}`,
51
+ who: l.sellerName,
52
+ amount: l.price,
53
+ })),
54
+ ...offers.map((o) => {
55
+ const listing = byId.get(o.listingId);
56
+ return {
57
+ key: `o-${o.id}`,
58
+ kind: o.status === "accepted" ? ("sold" as const) : ("offer" as const),
59
+ at: o.createdAt,
60
+ listing,
61
+ title: o.listingTitle,
62
+ href: `/listing/${listing?.slug || o.listingId}`,
63
+ who: o.buyerName,
64
+ amount: o.amount,
65
+ };
66
+ }),
67
+ ]
68
+ .sort((a, b) => Date.parse(b.at) - Date.parse(a.at))
69
+ .slice(0, limit);
70
+
71
+ return {
72
+ events,
73
+ forSale: listings.filter((l) => l.status === "active").length,
74
+ openOffers: offers.filter((o) => o.status === "pending").length,
75
+ loading,
76
+ };
77
+ }
78
+
79
+ function Thumb({ listing, className }: { listing?: Listing; className?: string }) {
80
+ return (
81
+ <span
82
+ className={cn(
83
+ "relative block shrink-0 overflow-hidden rounded-lg bg-muted",
84
+ className,
85
+ )}
86
+ style={
87
+ listing && !listing.imageUrl
88
+ ? { background: gradient(listing.seed || listing.id) }
89
+ : undefined
90
+ }
91
+ >
92
+ {listing?.imageUrl ? (
93
+ <img
94
+ src={listing.imageUrl}
95
+ srcSet={listingSrcSet(listing.imageUrl)}
96
+ sizes="48px"
97
+ alt=""
98
+ width="48"
99
+ height="60"
100
+ loading="lazy"
101
+ className="size-full object-cover"
102
+ />
103
+ ) : null}
104
+ <span
105
+ aria-hidden="true"
106
+ className="absolute inset-0 rounded-lg ring-1 ring-inset ring-black/5"
107
+ />
108
+ </span>
109
+ );
110
+ }
111
+
112
+ function Sentence({ event }: { event: ActivityEvent }) {
113
+ if (event.kind === "listed") {
114
+ return (
115
+ <>
116
+ <span className="font-medium text-foreground">{event.who}</span> listed{" "}
117
+ <span className="text-foreground">{event.title}</span>
118
+ </>
119
+ );
120
+ }
121
+ if (event.kind === "sold") {
122
+ return (
123
+ <>
124
+ <span className="text-foreground">{event.title}</span> sold for{" "}
125
+ <span className="font-medium tabular-nums text-foreground">
126
+ {money(event.amount)}
127
+ </span>
128
+ </>
129
+ );
130
+ }
131
+ return (
132
+ <>
133
+ <span className="font-medium text-foreground">{event.who}</span> offered{" "}
134
+ <span className="font-medium tabular-nums text-foreground">
135
+ {money(event.amount)}
136
+ </span>{" "}
137
+ on <span className="text-foreground">{event.title}</span>
138
+ </>
139
+ );
140
+ }
141
+
142
+ function Tag({ kind }: { kind: ActivityEvent["kind"] }) {
143
+ const label = { listed: "Listed", offer: "Offer", sold: "Sold" }[kind];
144
+ return (
145
+ <span
146
+ className={cn(
147
+ "rounded-full px-1.5 py-px text-[10px] font-semibold",
148
+ kind === "offer" && "bg-signal/12 text-signal-foreground",
149
+ kind === "sold" && "bg-foreground text-background",
150
+ kind === "listed" && "bg-muted text-muted-foreground",
151
+ )}
152
+ >
153
+ {label}
154
+ </span>
155
+ );
156
+ }
157
+
158
+ function LiveDot() {
159
+ return (
160
+ <span className="relative flex size-2" aria-hidden="true">
161
+ <span className="market-ping absolute inline-flex size-full rounded-full bg-signal" />
162
+ <span className="relative inline-flex size-2 rounded-full bg-signal" />
163
+ </span>
164
+ );
165
+ }
166
+
167
+ function Rail() {
168
+ const { events, forSale, openOffers, loading } = useEvents(9);
169
+ // Keys rendered on the first settled pass. Later keys arrived live.
170
+ const seen = useRef<Set<string> | null>(null);
171
+ if (seen.current === null && !loading) {
172
+ seen.current = new Set(events.map((e) => e.key));
173
+ }
174
+
175
+ return (
176
+ <section
177
+ aria-labelledby="activity-heading"
178
+ className="rounded-2xl bg-card shadow-[var(--shadow-border)]"
179
+ >
180
+ <div className="flex items-center justify-between px-4 pb-3 pt-4">
181
+ <h2
182
+ id="activity-heading"
183
+ className="flex items-center gap-2 text-sm font-semibold"
184
+ >
185
+ <LiveDot />
186
+ Live activity
187
+ </h2>
188
+ </div>
189
+ <dl className="mx-4 grid grid-cols-2 gap-px overflow-hidden rounded-xl bg-border">
190
+ <div className="bg-card px-3 py-2.5">
191
+ <dt className="text-xs text-muted-foreground">For sale</dt>
192
+ <dd className="font-display text-[28px] leading-8 tabular-nums">
193
+ {forSale}
194
+ </dd>
195
+ </div>
196
+ <div className="bg-card px-3 py-2.5">
197
+ <dt className="text-xs text-muted-foreground">Open offers</dt>
198
+ <dd className="font-display text-[28px] leading-8 tabular-nums">
199
+ {openOffers}
200
+ </dd>
201
+ </div>
202
+ </dl>
203
+ <ol aria-live="polite" className="mt-2 flex flex-col px-2 pb-2">
204
+ {events.map((event) => {
205
+ const fresh = seen.current !== null && !seen.current.has(event.key);
206
+ return (
207
+ <li key={event.key} className={cn(fresh && "market-rise")}>
208
+ <Link
209
+ href={event.href}
210
+ seed={event.listing}
211
+ className={cn(
212
+ "flex items-start gap-3 rounded-xl px-2 py-2 transition-colors hover:bg-accent",
213
+ fresh && "market-flash",
214
+ )}
215
+ >
216
+ <Thumb listing={event.listing} className="h-[50px] w-10" />
217
+ <span className="min-w-0 flex-1">
218
+ <span className="line-clamp-2 text-[13px] leading-[18px] text-muted-foreground">
219
+ <Sentence event={event} />
220
+ </span>
221
+ <span className="mt-1 flex items-center gap-2">
222
+ <Tag kind={event.kind} />
223
+ <span className="text-[11px] text-muted-foreground">
224
+ {timeAgo(event.at)}
225
+ </span>
226
+ </span>
227
+ </span>
228
+ </Link>
229
+ </li>
230
+ );
231
+ })}
232
+ </ol>
233
+ </section>
234
+ );
235
+ }
236
+
237
+ function Strip() {
238
+ const { events, loading } = useEvents(8);
239
+ const seen = useRef<Set<string> | null>(null);
240
+ if (seen.current === null && !loading) {
241
+ seen.current = new Set(events.map((e) => e.key));
242
+ }
243
+ if (events.length === 0) return null;
244
+
245
+ return (
246
+ <section aria-label="Live activity" className="-mx-4 sm:-mx-8">
247
+ <ol className="hide-scrollbar flex gap-2 overflow-x-auto px-4 sm:px-8">
248
+ <li className="flex shrink-0 items-center gap-2 pr-1 text-xs font-semibold">
249
+ <LiveDot />
250
+ Live
251
+ </li>
252
+ {events.map((event) => {
253
+ const fresh = seen.current !== null && !seen.current.has(event.key);
254
+ return (
255
+ <li key={event.key} className={cn("shrink-0", fresh && "market-rise")}>
256
+ <Link
257
+ href={event.href}
258
+ seed={event.listing}
259
+ className={cn(
260
+ "flex w-64 items-center gap-2.5 rounded-xl bg-card p-1.5 pr-3 shadow-[var(--shadow-border)]",
261
+ fresh && "market-flash",
262
+ )}
263
+ >
264
+ <Thumb listing={event.listing} className="h-10 w-8" />
265
+ <span className="min-w-0 flex-1 truncate text-xs text-muted-foreground">
266
+ <Sentence event={event} />
267
+ </span>
268
+ </Link>
269
+ </li>
270
+ );
271
+ })}
272
+ </ol>
273
+ </section>
274
+ );
275
+ }
276
+
277
+ export function ActivityRail() {
278
+ return (
279
+ <MarketProvider fallback={<RailSkeleton />}>
280
+ <Rail />
281
+ </MarketProvider>
282
+ );
283
+ }
284
+
285
+ export function ActivityStrip() {
286
+ return (
287
+ <MarketProvider fallback={<div className="h-[52px]" />}>
288
+ <Strip />
289
+ </MarketProvider>
290
+ );
291
+ }
292
+
293
+ function RailSkeleton() {
294
+ return (
295
+ <div className="rounded-2xl bg-card p-4 shadow-[var(--shadow-border)]">
296
+ <div className="h-4 w-28 rounded bg-muted" />
297
+ <div className="mt-4 h-16 rounded-xl bg-muted" />
298
+ <div className="mt-4 flex flex-col gap-3">
299
+ {Array.from({ length: 6 }).map((_, i) => (
300
+ <div key={i} className="flex gap-3">
301
+ <div className="h-[50px] w-10 rounded-lg bg-muted" />
302
+ <div className="flex flex-1 flex-col gap-2 pt-1">
303
+ <div className="h-3 rounded bg-muted" />
304
+ <div className="h-3 w-1/2 rounded bg-muted" />
305
+ </div>
306
+ </div>
307
+ ))}
308
+ </div>
309
+ </div>
310
+ );
311
+ }
@@ -2,40 +2,48 @@
2
2
 
3
3
  import React from "react";
4
4
  import { Link } from "@pylonsync/react";
5
- import { Button } from "@/components/ui/button";
6
5
  import { MarketProvider, useAuth } from "./MarketProvider";
6
+ import { avatarColor, initials } from "./market";
7
7
 
8
- // Compact sign-in state for the header. Signed out → a link to /sell (which
9
- // gates with the prefilled demo login). Signed in → your name + a sign-out
10
- // button. Lives behind MarketProvider like every other island.
8
+ // Sign-in state for the header. Signed out: a link to the dashboard, which
9
+ // shows the prefilled demo sign-in. Signed in: an avatar and a sign-out button.
11
10
  function Nav() {
12
11
  const { identity, signOut } = useAuth();
13
12
  if (!identity) {
14
13
  return (
15
- <Button asChild variant="outline" size="sm">
16
- <Link href="/sell">Sign in</Link>
17
- </Button>
14
+ <Link
15
+ href="/me"
16
+ className="inline-flex min-h-10 items-center rounded-full px-3.5 text-sm font-medium shadow-[var(--shadow-border)] transition-shadow hover:shadow-[var(--shadow-border-hover)]"
17
+ >
18
+ Sign in
19
+ </Link>
18
20
  );
19
21
  }
20
22
  return (
21
- <div className="flex items-center gap-2 text-sm">
22
- <span className="hidden text-muted-foreground sm:inline">
23
- {identity.name}
24
- </span>
25
- <Button
26
- variant="ghost"
27
- size="sm"
23
+ <div className="flex items-center gap-1">
24
+ <Link
25
+ href="/me"
26
+ aria-label={`Your market (${identity.name})`}
27
+ title={identity.name}
28
+ className="grid size-9 place-items-center rounded-full text-xs font-semibold text-white"
29
+ style={{ background: avatarColor(identity.name) }}
30
+ >
31
+ {initials(identity.name)}
32
+ </Link>
33
+ <button
34
+ type="button"
28
35
  onClick={() => void signOut()}
36
+ className="hidden min-h-10 items-center rounded-full px-3 text-sm font-medium text-muted-foreground transition-colors hover:bg-accent hover:text-foreground lg:inline-flex"
29
37
  >
30
38
  Sign out
31
- </Button>
39
+ </button>
32
40
  </div>
33
41
  );
34
42
  }
35
43
 
36
44
  export function AuthNav() {
37
45
  return (
38
- <MarketProvider fallback={<span className="w-16" />}>
46
+ <MarketProvider fallback={<span className="inline-block w-[72px]" />}>
39
47
  <Nav />
40
48
  </MarketProvider>
41
49
  );
@@ -0,0 +1,118 @@
1
+ import React from "react";
2
+ import { Link } from "@pylonsync/react";
3
+ import { cn } from "@/lib/utils";
4
+ import { listingSrcSet } from "../lib/catalog";
5
+ import { CategoryIcon } from "../app/_components/CategoryIcon";
6
+ import { WatchButton } from "./WatchButton";
7
+ import {
8
+ conditionLabel,
9
+ gradient,
10
+ money,
11
+ timeAgo,
12
+ type Listing,
13
+ } from "./market";
14
+
15
+ // One tile in the browse grid. Pure render (no data hooks), so the server
16
+ // grid and the live client grid draw the same markup.
17
+ export function ListingCard({
18
+ listing,
19
+ pendingOffers = 0,
20
+ fresh = false,
21
+ eager = false,
22
+ sizes = "(min-width: 1280px) 20vw, (min-width: 768px) 30vw, 50vw",
23
+ className,
24
+ }: {
25
+ listing: Listing;
26
+ /** Live count of pending offers; 0 hides the pill. */
27
+ pendingOffers?: number;
28
+ /** Arrived over sync after first paint. */
29
+ fresh?: boolean;
30
+ /** Load the photo eagerly (first row). */
31
+ eager?: boolean;
32
+ sizes?: string;
33
+ className?: string;
34
+ }) {
35
+ const sold = listing.status === "sold";
36
+ const href = `/listing/${listing.slug || listing.id}`;
37
+
38
+ return (
39
+ <article
40
+ className={cn(
41
+ "group relative min-w-0",
42
+ fresh && "market-rise",
43
+ className,
44
+ )}
45
+ >
46
+ <Link href={href} seed={listing} className="block rounded-xl">
47
+ <div className="relative aspect-[4/5] overflow-hidden rounded-xl bg-muted">
48
+ {listing.imageUrl ? (
49
+ <img
50
+ src={listing.imageUrl}
51
+ srcSet={listingSrcSet(listing.imageUrl)}
52
+ sizes={sizes}
53
+ alt={listing.title}
54
+ width="640"
55
+ height="800"
56
+ loading={eager ? "eager" : "lazy"}
57
+ fetchPriority={eager ? "high" : undefined}
58
+ decoding="async"
59
+ className="size-full object-cover transition-transform duration-700 ease-[var(--ease-out-soft)] group-hover:scale-[1.035]"
60
+ />
61
+ ) : (
62
+ <div
63
+ className="flex size-full items-center justify-center text-white/85"
64
+ style={{ background: gradient(listing.seed || listing.id) }}
65
+ >
66
+ <CategoryIcon category={listing.category} className="size-12" />
67
+ </div>
68
+ )}
69
+ <span
70
+ aria-hidden="true"
71
+ className="pointer-events-none absolute inset-0 rounded-xl ring-1 ring-inset ring-black/5"
72
+ />
73
+
74
+ {sold ? (
75
+ <span className="absolute inset-0 grid place-items-center bg-black/45">
76
+ <span className="rounded-full bg-white px-3.5 py-1 text-xs font-semibold text-neutral-900">
77
+ Sold
78
+ </span>
79
+ </span>
80
+ ) : fresh ? (
81
+ <span className="absolute left-2.5 top-2.5 inline-flex items-center gap-1.5 rounded-full bg-signal px-2.5 py-1 text-[11px] font-semibold text-white shadow-[var(--shadow-float)]">
82
+ Just listed
83
+ </span>
84
+ ) : pendingOffers > 0 ? (
85
+ <span className="absolute left-2.5 top-2.5 inline-flex items-center gap-1.5 rounded-full bg-white/92 px-2.5 py-1 text-[11px] font-medium text-neutral-900 shadow-[var(--shadow-float)] backdrop-blur">
86
+ <span className="size-1.5 rounded-full bg-signal" aria-hidden="true" />
87
+ {pendingOffers} {pendingOffers === 1 ? "offer" : "offers"}
88
+ </span>
89
+ ) : null}
90
+ </div>
91
+
92
+ <div className="flex flex-col gap-0.5 px-0.5 pt-2.5">
93
+ <div className="flex items-baseline justify-between gap-2">
94
+ <p className="text-[15px] font-semibold tabular-nums tracking-[-0.01em]">
95
+ {money(listing.price)}
96
+ </p>
97
+ <p className="shrink-0 text-xs text-muted-foreground" suppressHydrationWarning>
98
+ {timeAgo(listing.createdAt)}
99
+ </p>
100
+ </div>
101
+ <h3 className="truncate text-sm leading-5 text-foreground/90">
102
+ {listing.title}
103
+ </h3>
104
+ <p className="truncate text-[13px] leading-5 text-muted-foreground">
105
+ {[listing.location, conditionLabel(listing.condition)]
106
+ .filter(Boolean)
107
+ .join(" · ")}
108
+ </p>
109
+ </div>
110
+ </Link>
111
+ <WatchButton
112
+ listingId={listing.id}
113
+ listingTitle={listing.title}
114
+ className="absolute right-2.5 top-2.5"
115
+ />
116
+ </article>
117
+ );
118
+ }
@@ -0,0 +1,92 @@
1
+ "use client";
2
+
3
+ import React, { useRef } from "react";
4
+ import { db } from "@pylonsync/react";
5
+ import { browseListings, pendingOfferCounts } from "../lib/catalog";
6
+ import { ListingCard } from "./ListingCard";
7
+ import { MarketProvider } from "./MarketProvider";
8
+ import type { Listing, Offer } from "./market";
9
+
10
+ interface GridProps {
11
+ /** Active listings the server rendered. */
12
+ initial: Listing[];
13
+ /** Pending offers the server rendered, for the offer-count pills. */
14
+ initialOffers: Offer[];
15
+ category: string;
16
+ query: string;
17
+ sort: string;
18
+ }
19
+
20
+ const GRID =
21
+ "grid grid-cols-2 gap-x-3 gap-y-6 sm:gap-x-4 sm:gap-y-8 md:grid-cols-3 xl:grid-cols-4";
22
+
23
+ // The browse grid. The server renders it from `initial`; once the sync
24
+ // engine connects, the same grid switches to two live queries:
25
+ // - Listing: a listing posted in any tab rises into place with a
26
+ // "Just listed" pill, and one that sells gets a Sold overlay.
27
+ // - Offer: the "N offers" pill on each card follows pending offers.
28
+ export function LiveGrid(props: GridProps) {
29
+ return (
30
+ <MarketProvider fallback={<StaticGrid {...props} />}>
31
+ <Live {...props} />
32
+ </MarketProvider>
33
+ );
34
+ }
35
+
36
+ function StaticGrid({ initial, initialOffers, category, query, sort }: GridProps) {
37
+ const rows = browseListings(initial, { category, query, sort });
38
+ const counts = pendingOfferCounts(initialOffers);
39
+ return (
40
+ <div className={GRID}>
41
+ {rows.map((listing, index) => (
42
+ <ListingCard
43
+ key={listing.id}
44
+ listing={listing}
45
+ pendingOffers={counts.get(listing.id) ?? 0}
46
+ eager={index < 4}
47
+ />
48
+ ))}
49
+ </div>
50
+ );
51
+ }
52
+
53
+ function Live({ initial, initialOffers, category, query, sort }: GridProps) {
54
+ // Ids present at first paint. Anything else arrived live.
55
+ const firstIds = useRef<Set<string>>(new Set(initial.map((l) => l.id)));
56
+
57
+ const { data: liveListings } = db.useQuery<Listing>("Listing", {
58
+ orderBy: { createdAt: "desc" },
59
+ });
60
+ const { data: liveOffers, loading: offersLoading } = db.useQuery<Offer>(
61
+ "Offer",
62
+ { where: { status: "pending" } },
63
+ );
64
+
65
+ const byId = new Map(initial.map((l) => [l.id, l]));
66
+ for (const listing of liveListings ?? []) byId.set(listing.id, listing);
67
+ // Keep a listing that sells while on screen (it shows the Sold overlay);
68
+ // skip listings that were already sold before this page loaded.
69
+ const rows = browseListings(
70
+ [...byId.values()].filter(
71
+ (l) => l.status === "active" || firstIds.current.has(l.id),
72
+ ),
73
+ { category, query, sort },
74
+ );
75
+ const counts = pendingOfferCounts(
76
+ offersLoading ? initialOffers : (liveOffers ?? []),
77
+ );
78
+
79
+ return (
80
+ <div className={GRID}>
81
+ {rows.map((listing, index) => (
82
+ <ListingCard
83
+ key={listing.id}
84
+ listing={listing}
85
+ pendingOffers={counts.get(listing.id) ?? 0}
86
+ fresh={!firstIds.current.has(listing.id)}
87
+ eager={index < 4}
88
+ />
89
+ ))}
90
+ </div>
91
+ );
92
+ }