@pylonsync/create-pylon 0.19.0 → 0.20.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (675) hide show
  1. package/bin/create-pylon.js +30 -7
  2. package/package.json +1 -1
  3. package/templates/agency/.env.example +7 -0
  4. package/templates/agency/README.md +28 -3
  5. package/templates/agency/app/(marketing)/contact-form.tsx +20 -18
  6. package/templates/agency/app/(marketing)/page.tsx +3 -3
  7. package/templates/agency/app/(marketing)/work/page.tsx +3 -3
  8. package/templates/agency/app/auth-form.tsx +4 -4
  9. package/templates/agency/app/dashboard/dashboard-client.tsx +110 -64
  10. package/templates/agency/app/dashboard/page.tsx +14 -16
  11. package/templates/agency/app/icon.svg +3 -0
  12. package/templates/agency/app/login/page.tsx +12 -14
  13. package/templates/agency/components/marketing.tsx +12 -5
  14. package/templates/agency/functions/seedStudioBackoffice.ts +42 -11
  15. package/templates/agency/lib/agency.ts +22 -0
  16. package/templates/agency/lib/demo.ts +118 -0
  17. package/templates/agency/lib/site.config.ts +19 -13
  18. package/templates/agency/public/favicon.ico +0 -0
  19. package/templates/agency/tests/agency.test.ts +57 -0
  20. package/templates/ai-chat/.env.example +13 -27
  21. package/templates/ai-chat/README.md +53 -60
  22. package/templates/ai-chat/app/(chat)/c/[id]/page.tsx +19 -0
  23. package/templates/ai-chat/app/(chat)/chat-client.tsx +657 -622
  24. package/templates/ai-chat/app/(chat)/layout.tsx +6 -43
  25. package/templates/ai-chat/app/(chat)/not-found.tsx +6 -10
  26. package/templates/ai-chat/app/(chat)/page.tsx +7 -11
  27. package/templates/ai-chat/app/auth-form.tsx +45 -38
  28. package/templates/ai-chat/app/error.tsx +9 -12
  29. package/templates/ai-chat/app/globals.css +464 -102
  30. package/templates/ai-chat/app/layout.tsx +11 -19
  31. package/templates/ai-chat/app/login/page.tsx +39 -24
  32. package/templates/ai-chat/app.ts +118 -43
  33. package/templates/ai-chat/components/chat/composer.tsx +193 -0
  34. package/templates/ai-chat/components/chat/empty-state.tsx +65 -0
  35. package/templates/ai-chat/components/chat/icons.tsx +198 -0
  36. package/templates/ai-chat/components/chat/markdown.tsx +164 -0
  37. package/templates/ai-chat/components/chat/message.tsx +183 -0
  38. package/templates/ai-chat/components/chat/setup.tsx +71 -0
  39. package/templates/ai-chat/components/chat/sidebar.tsx +288 -0
  40. package/templates/ai-chat/functions/_beginTurn.ts +145 -0
  41. package/templates/ai-chat/functions/_finishTurn.ts +55 -0
  42. package/templates/ai-chat/functions/assistantStatus.ts +13 -0
  43. package/templates/ai-chat/functions/claimGuestHistory.ts +39 -0
  44. package/templates/ai-chat/functions/createConversation.ts +28 -0
  45. package/templates/ai-chat/functions/deleteConversation.ts +15 -20
  46. package/templates/ai-chat/functions/prepareGuestClaim.ts +32 -0
  47. package/templates/ai-chat/functions/renameConversation.ts +19 -0
  48. package/templates/ai-chat/functions/respond.ts +88 -0
  49. package/templates/ai-chat/functions/seedExamples.ts +46 -0
  50. package/templates/ai-chat/functions/stopResponse.ts +30 -0
  51. package/templates/ai-chat/lib/brand-icon.ts +7 -0
  52. package/templates/ai-chat/lib/chat.ts +277 -0
  53. package/templates/ai-chat/lib/claim.ts +15 -0
  54. package/templates/ai-chat/lib/examples.ts +179 -0
  55. package/templates/ai-chat/lib/highlight.ts +107 -0
  56. package/templates/ai-chat/lib/markdown.ts +317 -0
  57. package/templates/ai-chat/lib/provider.ts +38 -0
  58. package/templates/ai-chat/lib/site.config.ts +55 -39
  59. package/templates/ai-chat/tests/chat.test.ts +174 -0
  60. package/templates/ai-chat/tests/components.test.tsx +57 -0
  61. package/templates/ai-chat/tests/markdown.test.ts +88 -0
  62. package/templates/ai-chat/tests/provider.test.ts +32 -0
  63. package/templates/backend/consumer/apps/api/app/layout.tsx +33 -0
  64. package/templates/backend/consumer/apps/api/app/page.tsx +54 -0
  65. package/templates/backend/consumer/apps/api/app.ts +166 -0
  66. package/templates/backend/consumer/apps/api/functions/addComment.ts +24 -0
  67. package/templates/backend/consumer/apps/api/functions/createPost.ts +19 -36
  68. package/templates/backend/consumer/apps/api/functions/deletePost.ts +15 -13
  69. package/templates/backend/consumer/apps/api/functions/myProfile.ts +5 -8
  70. package/templates/backend/consumer/apps/api/functions/seedDemo.ts +95 -0
  71. package/templates/backend/consumer/apps/api/functions/setFollow.ts +36 -0
  72. package/templates/backend/consumer/apps/api/functions/setLike.ts +36 -0
  73. package/templates/backend/consumer/apps/api/functions/upsertProfile.ts +43 -37
  74. package/templates/backend/consumer/apps/api/lib/seed.ts +89 -0
  75. package/templates/backend/consumer/apps/api/lib/social.ts +50 -0
  76. package/templates/backend/consumer/apps/api/package.json +8 -2
  77. package/templates/backend/consumer/apps/api/public/images/posts/arch.jpg +0 -0
  78. package/templates/backend/consumer/apps/api/public/images/posts/coffee.jpg +0 -0
  79. package/templates/backend/consumer/apps/api/public/images/posts/dog.jpg +0 -0
  80. package/templates/backend/consumer/apps/api/public/images/posts/lisbon.jpg +0 -0
  81. package/templates/backend/consumer/apps/api/public/images/posts/ramen.jpg +0 -0
  82. package/templates/backend/consumer/apps/api/public/images/posts/run.jpg +0 -0
  83. package/templates/backend/consumer/apps/api/public/images/posts/sunset.jpg +0 -0
  84. package/templates/backend/consumer/apps/api/tsconfig.json +12 -2
  85. package/templates/chat/README.md +43 -21
  86. package/templates/chat/app/chat-client.tsx +495 -85
  87. package/templates/chat/app/error.tsx +1 -1
  88. package/templates/chat/app/globals.css +170 -54
  89. package/templates/chat/app/layout.tsx +12 -9
  90. package/templates/chat/app/not-found.tsx +4 -2
  91. package/templates/chat/app/page.tsx +9 -19
  92. package/templates/chat/app.ts +96 -17
  93. package/templates/chat/components/chat/avatar.tsx +45 -0
  94. package/templates/chat/components/chat/composer.tsx +110 -0
  95. package/templates/chat/components/chat/message-list.tsx +285 -0
  96. package/templates/chat/components/chat/sidebar.tsx +190 -0
  97. package/templates/chat/functions/joinChat.ts +83 -0
  98. package/templates/chat/functions/renameMember.ts +25 -0
  99. package/templates/chat/lib/chat.ts +188 -0
  100. package/templates/chat/lib/seed.ts +78 -0
  101. package/templates/chat/tests/chat.test.ts +159 -0
  102. package/templates/chat/tests/composer.test.tsx +54 -0
  103. package/templates/consumer/README.md +3 -3
  104. package/templates/consumer/app/globals.css +10 -3
  105. package/templates/consumer/app/icon.svg +1 -0
  106. package/templates/consumer/components/social/avatar.tsx +25 -11
  107. package/templates/consumer/functions/seedFeed.ts +3 -3
  108. package/templates/consumer/lib/seed.ts +43 -85
  109. package/templates/consumer/public/favicon.ico +0 -0
  110. package/templates/consumer/public/images/posts/arch.jpg +0 -0
  111. package/templates/consumer/public/images/posts/coffee.jpg +0 -0
  112. package/templates/consumer/public/images/posts/dog.jpg +0 -0
  113. package/templates/consumer/public/images/posts/lisbon.jpg +0 -0
  114. package/templates/consumer/public/images/posts/ramen.jpg +0 -0
  115. package/templates/consumer/public/images/posts/run.jpg +0 -0
  116. package/templates/consumer/public/images/posts/sunset.jpg +0 -0
  117. package/templates/consumer/tests/seed.test.ts +39 -0
  118. package/templates/creator/.env.example +7 -0
  119. package/templates/creator/README.md +24 -5
  120. package/templates/creator/app/(marketing)/newsletter-signup.tsx +33 -12
  121. package/templates/creator/app/auth-form.tsx +7 -7
  122. package/templates/creator/app/dashboard/dashboard-client.tsx +137 -100
  123. package/templates/creator/app/dashboard/page.tsx +18 -18
  124. package/templates/creator/app/icon.svg +2 -0
  125. package/templates/creator/app/login/page.tsx +14 -14
  126. package/templates/creator/app.ts +4 -4
  127. package/templates/creator/functions/seedDemo.ts +32 -0
  128. package/templates/creator/functions/subscribe.ts +4 -15
  129. package/templates/creator/lib/demo.ts +94 -0
  130. package/templates/creator/lib/site.config.ts +15 -10
  131. package/templates/creator/lib/stats.ts +35 -0
  132. package/templates/creator/public/favicon.ico +0 -0
  133. package/templates/creator/tests/counter.test.ts +92 -0
  134. package/templates/creator/tests/live-count.test.tsx +45 -0
  135. package/templates/crm/README.md +22 -1
  136. package/templates/crm/app/apple-icon.png +0 -0
  137. package/templates/crm/app/companies/companies-view.tsx +32 -14
  138. package/templates/crm/app/companies/page.tsx +2 -1
  139. package/templates/crm/app/contacts/contacts-view.tsx +34 -10
  140. package/templates/crm/app/contacts/page.tsx +2 -1
  141. package/templates/crm/app/deals/[id]/deal-view.tsx +137 -98
  142. package/templates/crm/app/deals/[id]/page.tsx +2 -1
  143. package/templates/crm/app/globals.css +29 -14
  144. package/templates/crm/app/icon.svg +1 -0
  145. package/templates/crm/app/layout.tsx +7 -5
  146. package/templates/crm/app/page.tsx +2 -1
  147. package/templates/crm/app/pipeline-view.tsx +5 -5
  148. package/templates/crm/app/workspace.tsx +29 -17
  149. package/templates/crm/app.ts +5 -5
  150. package/templates/crm/components/activity-timeline.tsx +14 -4
  151. package/templates/crm/components/app-shell.tsx +94 -0
  152. package/templates/crm/components/auth-form.tsx +35 -5
  153. package/templates/crm/components/avatar.tsx +5 -1
  154. package/templates/crm/components/board-skeleton.tsx +5 -5
  155. package/templates/crm/components/brand-mark.tsx +25 -0
  156. package/templates/crm/components/command-palette.tsx +17 -17
  157. package/templates/crm/components/data-table.tsx +97 -36
  158. package/templates/crm/components/deal-card.tsx +23 -27
  159. package/templates/crm/components/filter-tabs.tsx +101 -0
  160. package/templates/crm/components/metrics-bar.tsx +6 -6
  161. package/templates/crm/components/nav.tsx +15 -0
  162. package/templates/crm/components/page-header.tsx +43 -7
  163. package/templates/crm/components/pipeline-board.tsx +16 -10
  164. package/templates/crm/components/require-auth.tsx +24 -26
  165. package/templates/crm/components/sidebar.tsx +54 -49
  166. package/templates/crm/components/stage-badge.tsx +4 -2
  167. package/templates/crm/components/ui/button.tsx +5 -4
  168. package/templates/crm/components/ui/dialog.tsx +1 -1
  169. package/templates/crm/components/ui/input.tsx +1 -1
  170. package/templates/crm/components/ui/select.tsx +1 -1
  171. package/templates/crm/components/ui/textarea.tsx +1 -1
  172. package/templates/crm/functions/seedWorkspace.ts +32 -17
  173. package/templates/crm/lib/brand.ts +9 -0
  174. package/templates/crm/lib/demo-team.ts +51 -0
  175. package/templates/crm/lib/pipeline.ts +37 -0
  176. package/templates/crm/lib/search.ts +4 -3
  177. package/templates/crm/lib/seed.ts +53 -8
  178. package/templates/crm/public/favicon.ico +0 -0
  179. package/templates/crm/tests/components.test.tsx +5 -5
  180. package/templates/crm/tests/demo-team.test.ts +18 -0
  181. package/templates/crm/tests/pipeline.test.ts +59 -3
  182. package/templates/directory/.env.example +7 -0
  183. package/templates/directory/README.md +22 -3
  184. package/templates/directory/app/(marketing)/directory-browse.tsx +30 -12
  185. package/templates/directory/app/(marketing)/page.tsx +6 -4
  186. package/templates/directory/app/auth-form.tsx +4 -4
  187. package/templates/directory/app/dashboard/dashboard-client.tsx +177 -90
  188. package/templates/directory/app/dashboard/page.tsx +16 -14
  189. package/templates/directory/app/icon.svg +4 -0
  190. package/templates/directory/app/login/page.tsx +16 -15
  191. package/templates/directory/functions/seedDemo.ts +43 -0
  192. package/templates/directory/lib/demo.ts +145 -0
  193. package/templates/directory/lib/directory.ts +27 -0
  194. package/templates/directory/lib/site.config.ts +21 -16
  195. package/templates/directory/public/favicon.ico +0 -0
  196. package/templates/directory/tests/directory.test.ts +46 -0
  197. package/templates/expo/consumer/apps/expo/App.tsx +360 -288
  198. package/templates/expo/consumer/apps/expo/app.json +9 -1
  199. package/templates/expo/consumer/apps/expo/package.json +2 -1
  200. package/templates/helpdesk/README.md +21 -0
  201. package/templates/helpdesk/app/apple-icon.png +0 -0
  202. package/templates/helpdesk/app/customers/customers-view.tsx +23 -7
  203. package/templates/helpdesk/app/customers/page.tsx +2 -1
  204. package/templates/helpdesk/app/globals.css +29 -14
  205. package/templates/helpdesk/app/icon.svg +1 -0
  206. package/templates/helpdesk/app/inbox-view.tsx +66 -47
  207. package/templates/helpdesk/app/layout.tsx +7 -5
  208. package/templates/helpdesk/app/page.tsx +4 -7
  209. package/templates/helpdesk/app/tickets/[id]/page.tsx +2 -1
  210. package/templates/helpdesk/app/tickets/[id]/ticket-view.tsx +134 -97
  211. package/templates/helpdesk/app/workspace.tsx +33 -22
  212. package/templates/helpdesk/app.ts +2 -2
  213. package/templates/helpdesk/components/app-shell.tsx +94 -0
  214. package/templates/helpdesk/components/auth-form.tsx +35 -5
  215. package/templates/helpdesk/components/avatar.tsx +5 -1
  216. package/templates/helpdesk/components/brand-mark.tsx +28 -0
  217. package/templates/helpdesk/components/command-palette.tsx +17 -17
  218. package/templates/helpdesk/components/data-table.tsx +97 -36
  219. package/templates/helpdesk/components/filter-tabs.tsx +101 -0
  220. package/templates/helpdesk/components/nav.tsx +14 -0
  221. package/templates/helpdesk/components/page-header.tsx +43 -7
  222. package/templates/helpdesk/components/priority-badge.tsx +49 -6
  223. package/templates/helpdesk/components/require-auth.tsx +24 -26
  224. package/templates/helpdesk/components/sidebar.tsx +54 -48
  225. package/templates/helpdesk/components/sla-indicator.tsx +5 -8
  226. package/templates/helpdesk/components/thread.tsx +7 -7
  227. package/templates/helpdesk/components/ticket-list.tsx +79 -19
  228. package/templates/helpdesk/components/ui/button.tsx +5 -4
  229. package/templates/helpdesk/components/ui/dialog.tsx +1 -1
  230. package/templates/helpdesk/components/ui/input.tsx +1 -1
  231. package/templates/helpdesk/components/ui/select.tsx +1 -1
  232. package/templates/helpdesk/components/ui/textarea.tsx +1 -1
  233. package/templates/helpdesk/functions/seedWorkspace.ts +25 -15
  234. package/templates/helpdesk/lib/brand.ts +9 -0
  235. package/templates/helpdesk/lib/demo-team.ts +51 -0
  236. package/templates/helpdesk/lib/search.ts +4 -3
  237. package/templates/helpdesk/lib/seed.ts +43 -2
  238. package/templates/helpdesk/lib/tickets.ts +68 -20
  239. package/templates/helpdesk/public/favicon.ico +0 -0
  240. package/templates/helpdesk/tests/components.test.tsx +10 -0
  241. package/templates/helpdesk/tests/demo-team.test.ts +18 -0
  242. package/templates/helpdesk/tests/tickets.test.ts +70 -0
  243. package/templates/imessage/.env.example +49 -0
  244. package/templates/imessage/AGENTS.md +153 -0
  245. package/templates/imessage/CLAUDE.md +7 -0
  246. package/templates/imessage/README.md +187 -0
  247. package/templates/imessage/app/console.tsx +382 -0
  248. package/templates/imessage/app/contacts/page.tsx +13 -0
  249. package/templates/imessage/app/error.tsx +16 -0
  250. package/templates/imessage/app/gate.tsx +31 -0
  251. package/templates/imessage/app/globals.css +183 -0
  252. package/templates/imessage/app/layout.tsx +17 -0
  253. package/templates/imessage/app/login/login-form.tsx +115 -0
  254. package/templates/imessage/app/login/page.tsx +30 -0
  255. package/templates/imessage/app/not-found.tsx +13 -0
  256. package/templates/imessage/app/page.tsx +14 -0
  257. package/templates/imessage/app/robots.ts +6 -0
  258. package/templates/imessage/app/setup/page.tsx +13 -0
  259. package/templates/imessage/app/sign-out.tsx +20 -0
  260. package/templates/imessage/app.ts +299 -0
  261. package/templates/imessage/bunfig +4 -0
  262. package/templates/imessage/components/avatar.tsx +46 -0
  263. package/templates/imessage/components/contact-panel.tsx +263 -0
  264. package/templates/imessage/components/contacts-view.tsx +172 -0
  265. package/templates/imessage/components/conversation-list.tsx +129 -0
  266. package/templates/imessage/components/nav-rail.tsx +67 -0
  267. package/templates/imessage/components/setup-view.tsx +408 -0
  268. package/templates/imessage/components/thread.tsx +229 -0
  269. package/templates/imessage/components/ui/badge.tsx +37 -0
  270. package/templates/imessage/components/ui/button.tsx +56 -0
  271. package/templates/imessage/components/ui/card.tsx +90 -0
  272. package/templates/imessage/components/ui/dialog.tsx +110 -0
  273. package/templates/imessage/components/ui/input.tsx +30 -0
  274. package/templates/imessage/components/ui/label.tsx +21 -0
  275. package/templates/imessage/components/ui/select.tsx +39 -0
  276. package/templates/imessage/components/ui/table.tsx +124 -0
  277. package/templates/imessage/components/ui/textarea.tsx +24 -0
  278. package/templates/imessage/components.json +20 -0
  279. package/templates/imessage/functions/answerNow.ts +34 -0
  280. package/templates/imessage/functions/assistant.ts +96 -0
  281. package/templates/imessage/functions/beginTurn.ts +101 -0
  282. package/templates/imessage/functions/cancelReminder.ts +13 -0
  283. package/templates/imessage/functions/claimOutbound.ts +44 -0
  284. package/templates/imessage/functions/clearDemo.ts +18 -0
  285. package/templates/imessage/functions/deleteNote.ts +11 -0
  286. package/templates/imessage/functions/deliverOutbound.ts +55 -0
  287. package/templates/imessage/functions/finishTurn.ts +84 -0
  288. package/templates/imessage/functions/fireReminder.ts +42 -0
  289. package/templates/imessage/functions/ingestInbound.ts +22 -0
  290. package/templates/imessage/functions/ownerAccount.ts +13 -0
  291. package/templates/imessage/functions/processTurn.ts +74 -0
  292. package/templates/imessage/functions/relayApply.ts +102 -0
  293. package/templates/imessage/functions/relaySync.ts +63 -0
  294. package/templates/imessage/functions/releaseOutbound.ts +18 -0
  295. package/templates/imessage/functions/resolveTurn.ts +25 -0
  296. package/templates/imessage/functions/saveContact.ts +27 -0
  297. package/templates/imessage/functions/seedDemo.ts +70 -0
  298. package/templates/imessage/functions/sendOwnerMessage.ts +28 -0
  299. package/templates/imessage/functions/sendblueWebhook.ts +49 -0
  300. package/templates/imessage/functions/setContactStatus.ts +17 -0
  301. package/templates/imessage/functions/settleOutbound.ts +35 -0
  302. package/templates/imessage/functions/setupStatus.ts +33 -0
  303. package/templates/imessage/functions/sweep.ts +58 -0
  304. package/templates/imessage/functions/toolCancelReminder.ts +17 -0
  305. package/templates/imessage/functions/toolForgetNote.ts +15 -0
  306. package/templates/imessage/functions/toolListNotes.ts +16 -0
  307. package/templates/imessage/functions/toolListReminders.ts +17 -0
  308. package/templates/imessage/functions/toolSaveNote.ts +22 -0
  309. package/templates/imessage/functions/toolSetReminder.ts +55 -0
  310. package/templates/imessage/functions/updateSettings.ts +48 -0
  311. package/templates/imessage/gitignore +11 -0
  312. package/templates/imessage/lib/agent-call.ts +251 -0
  313. package/templates/imessage/lib/assistant.ts +74 -0
  314. package/templates/imessage/lib/chunk.ts +135 -0
  315. package/templates/imessage/lib/demo.ts +68 -0
  316. package/templates/imessage/lib/format.ts +170 -0
  317. package/templates/imessage/lib/gate.ts +104 -0
  318. package/templates/imessage/lib/handles.ts +67 -0
  319. package/templates/imessage/lib/provider.ts +38 -0
  320. package/templates/imessage/lib/relay-protocol.ts +95 -0
  321. package/templates/imessage/lib/reminders.ts +41 -0
  322. package/templates/imessage/lib/secrets.ts +59 -0
  323. package/templates/imessage/lib/sendblue.ts +173 -0
  324. package/templates/imessage/lib/store.ts +407 -0
  325. package/templates/imessage/lib/transport.ts +135 -0
  326. package/templates/imessage/lib/utils.ts +10 -0
  327. package/templates/imessage/package.json +40 -0
  328. package/templates/imessage/relay/.env.example +14 -0
  329. package/templates/imessage/relay/applescript.ts +70 -0
  330. package/templates/imessage/relay/chatdb.ts +165 -0
  331. package/templates/imessage/relay/client.ts +41 -0
  332. package/templates/imessage/relay/main.ts +137 -0
  333. package/templates/imessage/relay/state.ts +35 -0
  334. package/templates/imessage/tests/agent-call.test.ts +112 -0
  335. package/templates/imessage/tests/applescript.test.ts +88 -0
  336. package/templates/imessage/tests/chatdb.test.ts +139 -0
  337. package/templates/imessage/tests/chunk.test.ts +61 -0
  338. package/templates/imessage/tests/fake-ctx.ts +88 -0
  339. package/templates/imessage/tests/ingest.test.ts +141 -0
  340. package/templates/imessage/tests/lib.test.ts +99 -0
  341. package/templates/imessage/tests/review-fixes.test.ts +138 -0
  342. package/templates/imessage/tests/secrets.test.ts +36 -0
  343. package/templates/imessage/tests/sendblue.test.ts +143 -0
  344. package/templates/imessage/tests/setup.ts +5 -0
  345. package/templates/imessage/tsconfig.json +18 -0
  346. package/templates/inventory/README.md +21 -0
  347. package/templates/inventory/app/apple-icon.png +0 -0
  348. package/templates/inventory/app/globals.css +29 -14
  349. package/templates/inventory/app/icon.svg +1 -0
  350. package/templates/inventory/app/layout.tsx +7 -5
  351. package/templates/inventory/app/movements/movements-view.tsx +32 -10
  352. package/templates/inventory/app/movements/page.tsx +2 -1
  353. package/templates/inventory/app/page.tsx +4 -6
  354. package/templates/inventory/app/products/[id]/page.tsx +2 -1
  355. package/templates/inventory/app/products/[id]/product-view.tsx +55 -25
  356. package/templates/inventory/app/products-view.tsx +74 -33
  357. package/templates/inventory/app/workspace.tsx +34 -16
  358. package/templates/inventory/app.ts +10 -2
  359. package/templates/inventory/components/app-shell.tsx +94 -0
  360. package/templates/inventory/components/auth-form.tsx +35 -5
  361. package/templates/inventory/components/avatar.tsx +5 -1
  362. package/templates/inventory/components/brand-mark.tsx +25 -0
  363. package/templates/inventory/components/category-tile.tsx +49 -0
  364. package/templates/inventory/components/command-palette.tsx +17 -17
  365. package/templates/inventory/components/data-table.tsx +97 -36
  366. package/templates/inventory/components/filter-tabs.tsx +101 -0
  367. package/templates/inventory/components/nav.tsx +14 -0
  368. package/templates/inventory/components/page-header.tsx +43 -7
  369. package/templates/inventory/components/require-auth.tsx +24 -26
  370. package/templates/inventory/components/sidebar.tsx +54 -48
  371. package/templates/inventory/components/stock-level.tsx +30 -14
  372. package/templates/inventory/components/summary-bar.tsx +8 -15
  373. package/templates/inventory/components/ui/button.tsx +5 -4
  374. package/templates/inventory/components/ui/dialog.tsx +1 -1
  375. package/templates/inventory/components/ui/input.tsx +1 -1
  376. package/templates/inventory/components/ui/select.tsx +1 -1
  377. package/templates/inventory/components/ui/textarea.tsx +1 -1
  378. package/templates/inventory/functions/seedWorkspace.ts +18 -8
  379. package/templates/inventory/lib/brand.ts +9 -0
  380. package/templates/inventory/lib/demo-team.ts +51 -0
  381. package/templates/inventory/lib/search.ts +4 -3
  382. package/templates/inventory/lib/seed.ts +29 -2
  383. package/templates/inventory/lib/stock.ts +32 -0
  384. package/templates/inventory/public/favicon.ico +0 -0
  385. package/templates/inventory/tests/demo-team.test.ts +18 -0
  386. package/templates/inventory/tests/stock.test.ts +29 -0
  387. package/templates/invoices/README.md +16 -0
  388. package/templates/invoices/app/apple-icon.png +0 -0
  389. package/templates/invoices/app/clients/clients-view.tsx +18 -6
  390. package/templates/invoices/app/clients/page.tsx +2 -1
  391. package/templates/invoices/app/globals.css +29 -14
  392. package/templates/invoices/app/icon.svg +1 -0
  393. package/templates/invoices/app/invoices/[id]/invoice-view.tsx +72 -48
  394. package/templates/invoices/app/invoices/[id]/page.tsx +2 -1
  395. package/templates/invoices/app/invoices-view.tsx +81 -32
  396. package/templates/invoices/app/layout.tsx +7 -5
  397. package/templates/invoices/app/page.tsx +4 -6
  398. package/templates/invoices/app/workspace.tsx +26 -16
  399. package/templates/invoices/app.ts +2 -2
  400. package/templates/invoices/components/app-shell.tsx +94 -0
  401. package/templates/invoices/components/auth-form.tsx +35 -5
  402. package/templates/invoices/components/avatar.tsx +5 -1
  403. package/templates/invoices/components/brand-mark.tsx +26 -0
  404. package/templates/invoices/components/command-palette.tsx +17 -17
  405. package/templates/invoices/components/data-table.tsx +97 -36
  406. package/templates/invoices/components/filter-tabs.tsx +101 -0
  407. package/templates/invoices/components/line-items.tsx +19 -14
  408. package/templates/invoices/components/nav.tsx +14 -0
  409. package/templates/invoices/components/page-header.tsx +43 -7
  410. package/templates/invoices/components/require-auth.tsx +24 -26
  411. package/templates/invoices/components/sidebar.tsx +54 -48
  412. package/templates/invoices/components/status-badge.tsx +7 -7
  413. package/templates/invoices/components/summary-bar.tsx +5 -5
  414. package/templates/invoices/components/ui/button.tsx +5 -4
  415. package/templates/invoices/components/ui/dialog.tsx +1 -1
  416. package/templates/invoices/components/ui/input.tsx +1 -1
  417. package/templates/invoices/components/ui/select.tsx +1 -1
  418. package/templates/invoices/components/ui/textarea.tsx +1 -1
  419. package/templates/invoices/functions/recordPayment.ts +3 -0
  420. package/templates/invoices/lib/billing.ts +40 -0
  421. package/templates/invoices/lib/brand.ts +9 -0
  422. package/templates/invoices/lib/search.ts +4 -3
  423. package/templates/invoices/public/favicon.ico +0 -0
  424. package/templates/invoices/tests/billing.test.ts +27 -0
  425. package/templates/ios/consumer/apps/ios/Package.swift +4 -3
  426. package/templates/ios/consumer/apps/ios/Sources/__APP_NAME_PASCAL__/ActivityView.swift +107 -0
  427. package/templates/ios/consumer/apps/ios/Sources/__APP_NAME_PASCAL__/AppModel.swift +171 -0
  428. package/templates/ios/consumer/apps/ios/Sources/__APP_NAME_PASCAL__/AuthView.swift +182 -0
  429. package/templates/ios/consumer/apps/ios/Sources/__APP_NAME_PASCAL__/CommentsView.swift +136 -0
  430. package/templates/ios/consumer/apps/ios/Sources/__APP_NAME_PASCAL__/ComposeView.swift +152 -0
  431. package/templates/ios/consumer/apps/ios/Sources/__APP_NAME_PASCAL__/ExploreView.swift +117 -0
  432. package/templates/ios/consumer/apps/ios/Sources/__APP_NAME_PASCAL__/FeedView.swift +175 -159
  433. package/templates/ios/consumer/apps/ios/Sources/__APP_NAME_PASCAL__/MainTabView.swift +54 -0
  434. package/templates/ios/consumer/apps/ios/Sources/__APP_NAME_PASCAL__/Media.swift +271 -0
  435. package/templates/ios/consumer/apps/ios/Sources/__APP_NAME_PASCAL__/Models.swift +101 -4
  436. package/templates/ios/consumer/apps/ios/Sources/__APP_NAME_PASCAL__/PostCard.swift +245 -0
  437. package/templates/ios/consumer/apps/ios/Sources/__APP_NAME_PASCAL__/ProfileEditorView.swift +208 -0
  438. package/templates/ios/consumer/apps/ios/Sources/__APP_NAME_PASCAL__/ProfileView.swift +183 -0
  439. package/templates/ios/consumer/apps/ios/Sources/__APP_NAME_PASCAL__/RootView.swift +25 -31
  440. package/templates/ios/consumer/apps/ios/Sources/__APP_NAME_PASCAL__/SocialStore.swift +285 -0
  441. package/templates/ios/consumer/apps/ios/Sources/__APP_NAME_PASCAL__/StoryViewer.swift +155 -0
  442. package/templates/ios/consumer/apps/ios/Sources/__APP_NAME_PASCAL__/Theme.swift +82 -0
  443. package/templates/ios/consumer/apps/ios/Sources/__APP_NAME_PASCAL__/__APP_NAME_PASCAL__App.swift +4 -47
  444. package/templates/ios/consumer/apps/ios/project.yml +13 -9
  445. package/templates/local-service/.env.example +7 -0
  446. package/templates/local-service/README.md +26 -2
  447. package/templates/local-service/app/(marketing)/booking-widget.tsx +48 -9
  448. package/templates/local-service/app/(marketing)/page.tsx +66 -42
  449. package/templates/local-service/app/auth-form.tsx +8 -8
  450. package/templates/local-service/app/dashboard/dashboard-client.tsx +119 -92
  451. package/templates/local-service/app/dashboard/page.tsx +14 -14
  452. package/templates/local-service/app/icon.svg +2 -0
  453. package/templates/local-service/app/login/page.tsx +15 -15
  454. package/templates/local-service/functions/seedDemo.ts +49 -0
  455. package/templates/local-service/lib/demo.ts +135 -0
  456. package/templates/local-service/lib/hours.ts +44 -0
  457. package/templates/local-service/lib/site.config.ts +8 -3
  458. package/templates/local-service/lib/slots.ts +3 -0
  459. package/templates/local-service/public/favicon.ico +0 -0
  460. package/templates/local-service/tests/booking.test.ts +104 -0
  461. package/templates/marketplace/README.md +23 -15
  462. package/templates/marketplace/app/globals.css +172 -114
  463. package/templates/marketplace/app/layout.tsx +75 -30
  464. package/templates/marketplace/app/listing/[id]/page.tsx +210 -123
  465. package/templates/marketplace/app/me/page.tsx +4 -4
  466. package/templates/marketplace/app/page.tsx +221 -298
  467. package/templates/marketplace/app/sell/page.tsx +8 -11
  468. package/templates/marketplace/app.ts +32 -7
  469. package/templates/marketplace/client/ActivityFeed.tsx +311 -0
  470. package/templates/marketplace/client/AuthNav.tsx +24 -16
  471. package/templates/marketplace/client/ListingCard.tsx +118 -0
  472. package/templates/marketplace/client/LiveGrid.tsx +92 -0
  473. package/templates/marketplace/client/LiveListingStatus.tsx +32 -24
  474. package/templates/marketplace/client/LoginCard.tsx +114 -136
  475. package/templates/marketplace/client/MarketProvider.tsx +3 -5
  476. package/templates/marketplace/client/MyMarket.tsx +371 -185
  477. package/templates/marketplace/client/OfferPanel.tsx +421 -287
  478. package/templates/marketplace/client/SellForm.tsx +271 -192
  479. package/templates/marketplace/client/ThemeToggle.tsx +1 -0
  480. package/templates/marketplace/client/WatchButton.tsx +16 -16
  481. package/templates/marketplace/client/market.ts +61 -14
  482. package/templates/marketplace/functions/seedMarket.ts +143 -36
  483. package/templates/marketplace/functions/seedOffers.ts +101 -0
  484. package/templates/marketplace/lib/catalog.ts +72 -1
  485. package/templates/marketplace/public/images/listings/ceramic-lamp-640.webp +0 -0
  486. package/templates/marketplace/public/images/listings/ceramic-lamp.webp +0 -0
  487. package/templates/marketplace/public/images/listings/duffel-640.webp +0 -0
  488. package/templates/marketplace/public/images/listings/duffel.webp +0 -0
  489. package/templates/marketplace/public/images/listings/film-camera-640.webp +0 -0
  490. package/templates/marketplace/public/images/listings/film-camera.webp +0 -0
  491. package/templates/marketplace/public/images/listings/lounge-chair-640.webp +0 -0
  492. package/templates/marketplace/public/images/listings/lounge-chair.webp +0 -0
  493. package/templates/marketplace/public/images/listings/road-bike-640.webp +0 -0
  494. package/templates/marketplace/public/images/listings/road-bike.webp +0 -0
  495. package/templates/marketplace/public/images/listings/slipper-chair-640.webp +0 -0
  496. package/templates/marketplace/public/images/listings/slipper-chair.webp +0 -0
  497. package/templates/marketplace/public/images/listings/turntable-640.webp +0 -0
  498. package/templates/marketplace/public/images/listings/turntable.webp +0 -0
  499. package/templates/marketplace/public/images/listings/wardrobe-640.webp +0 -0
  500. package/templates/marketplace/public/images/listings/wardrobe.webp +0 -0
  501. package/templates/marketplace/public/images/listings/watch-640.webp +0 -0
  502. package/templates/marketplace/public/images/listings/watch.webp +0 -0
  503. package/templates/marketplace/public/images/listings/weekender-bag-640.webp +0 -0
  504. package/templates/marketplace/public/images/listings/weekender-bag.webp +0 -0
  505. package/templates/marketplace/tests/example.test.ts +64 -2
  506. package/templates/marketplace/tests/listing-card.test.tsx +61 -0
  507. package/templates/projects/README.md +24 -3
  508. package/templates/projects/app/apple-icon.png +0 -0
  509. package/templates/projects/app/clients/clients-view.tsx +20 -6
  510. package/templates/projects/app/clients/page.tsx +2 -1
  511. package/templates/projects/app/globals.css +29 -14
  512. package/templates/projects/app/icon.svg +1 -0
  513. package/templates/projects/app/layout.tsx +7 -5
  514. package/templates/projects/app/page.tsx +3 -5
  515. package/templates/projects/app/projects/[id]/page.tsx +2 -1
  516. package/templates/projects/app/projects/[id]/project-view.tsx +45 -30
  517. package/templates/projects/app/projects-view.tsx +105 -52
  518. package/templates/projects/app/workspace.tsx +41 -20
  519. package/templates/projects/app.ts +2 -2
  520. package/templates/projects/components/app-shell.tsx +94 -0
  521. package/templates/projects/components/auth-form.tsx +35 -5
  522. package/templates/projects/components/avatar.tsx +5 -1
  523. package/templates/projects/components/board-skeleton.tsx +3 -3
  524. package/templates/projects/components/brand-mark.tsx +25 -0
  525. package/templates/projects/components/command-palette.tsx +17 -17
  526. package/templates/projects/components/data-table.tsx +97 -36
  527. package/templates/projects/components/filter-tabs.tsx +101 -0
  528. package/templates/projects/components/meter.tsx +48 -0
  529. package/templates/projects/components/nav.tsx +14 -0
  530. package/templates/projects/components/page-header.tsx +43 -7
  531. package/templates/projects/components/require-auth.tsx +24 -26
  532. package/templates/projects/components/sidebar.tsx +54 -48
  533. package/templates/projects/components/summary-bar.tsx +49 -0
  534. package/templates/projects/components/task-board.tsx +10 -9
  535. package/templates/projects/components/time-dialog.tsx +29 -5
  536. package/templates/projects/components/ui/button.tsx +5 -4
  537. package/templates/projects/components/ui/dialog.tsx +1 -1
  538. package/templates/projects/components/ui/input.tsx +1 -1
  539. package/templates/projects/components/ui/select.tsx +1 -1
  540. package/templates/projects/components/ui/textarea.tsx +1 -1
  541. package/templates/projects/functions/seedWorkspace.ts +26 -12
  542. package/templates/projects/lib/brand.ts +9 -0
  543. package/templates/projects/lib/demo-team.ts +51 -0
  544. package/templates/projects/lib/search.ts +4 -3
  545. package/templates/projects/lib/seed.ts +35 -1
  546. package/templates/projects/lib/work.ts +63 -0
  547. package/templates/projects/public/favicon.ico +0 -0
  548. package/templates/projects/tests/demo-team.test.ts +18 -0
  549. package/templates/projects/tests/work.test.ts +65 -0
  550. package/templates/restaurant/.env.example +7 -0
  551. package/templates/restaurant/README.md +29 -3
  552. package/templates/restaurant/app/(marketing)/layout.tsx +4 -2
  553. package/templates/restaurant/app/(marketing)/page.tsx +47 -23
  554. package/templates/restaurant/app/(marketing)/reservation-widget.tsx +37 -7
  555. package/templates/restaurant/app/auth-form.tsx +8 -8
  556. package/templates/restaurant/app/dashboard/dashboard-client.tsx +241 -109
  557. package/templates/restaurant/app/dashboard/page.tsx +14 -14
  558. package/templates/restaurant/app/icon.svg +4 -0
  559. package/templates/restaurant/app/login/page.tsx +15 -15
  560. package/templates/restaurant/functions/seedDemo.ts +44 -0
  561. package/templates/restaurant/lib/demo.ts +144 -0
  562. package/templates/restaurant/lib/hours.ts +45 -0
  563. package/templates/restaurant/lib/site.config.ts +3 -3
  564. package/templates/restaurant/public/favicon.ico +0 -0
  565. package/templates/restaurant/tests/reservations.test.ts +67 -0
  566. package/templates/saas/README.md +54 -35
  567. package/templates/saas/app/(auth)/auth-form.tsx +25 -23
  568. package/templates/saas/app/(auth)/forgot-password/page.tsx +1 -1
  569. package/templates/saas/app/(auth)/layout.tsx +27 -33
  570. package/templates/saas/app/(auth)/login/page.tsx +1 -1
  571. package/templates/saas/app/(auth)/reset-password/page.tsx +1 -1
  572. package/templates/saas/app/(auth)/signup/page.tsx +1 -1
  573. package/templates/saas/app/(marketing)/layout.tsx +127 -253
  574. package/templates/saas/app/(marketing)/page.tsx +133 -372
  575. package/templates/saas/app/(marketing)/pricing/page.tsx +21 -19
  576. package/templates/saas/app/(marketing)/products/[slug]/page.tsx +40 -95
  577. package/templates/saas/app/dashboard/board-client.tsx +416 -0
  578. package/templates/saas/app/dashboard/dashboard-client.tsx +30 -615
  579. package/templates/saas/app/dashboard/layout.tsx +27 -13
  580. package/templates/saas/app/dashboard/overview-client.tsx +332 -0
  581. package/templates/saas/app/dashboard/page.tsx +29 -35
  582. package/templates/saas/app/dashboard/projects/[id]/page.tsx +29 -0
  583. package/templates/saas/app/dashboard/projects/page.tsx +11 -7
  584. package/templates/saas/app/dashboard/projects-client.tsx +398 -0
  585. package/templates/saas/app/globals.css +5 -4
  586. package/templates/saas/app/icon.svg +1 -0
  587. package/templates/saas/app/onboarding/onboarding-client.tsx +93 -26
  588. package/templates/saas/app/onboarding/page.tsx +6 -2
  589. package/templates/saas/app.ts +68 -13
  590. package/templates/saas/components/dashboard-shell.tsx +212 -132
  591. package/templates/saas/components/marketing.tsx +137 -274
  592. package/templates/saas/components/pricing-table.tsx +12 -11
  593. package/templates/saas/components/setup-checklist.tsx +31 -29
  594. package/templates/saas/components/task-ui.tsx +199 -0
  595. package/templates/saas/functions/createProject.ts +7 -13
  596. package/templates/saas/functions/createTask.ts +67 -0
  597. package/templates/saas/functions/deleteProject.ts +17 -0
  598. package/templates/saas/functions/deleteTask.ts +14 -0
  599. package/templates/saas/functions/seedWorkspace.ts +46 -0
  600. package/templates/saas/functions/setProjectStatus.ts +41 -0
  601. package/templates/saas/functions/updateProfile.ts +13 -0
  602. package/templates/saas/functions/updateProject.ts +4 -5
  603. package/templates/saas/functions/updateTask.ts +88 -0
  604. package/templates/saas/lib/members.ts +53 -0
  605. package/templates/saas/lib/names.ts +53 -0
  606. package/templates/saas/lib/plans.ts +11 -0
  607. package/templates/saas/lib/sample-workspace.ts +117 -0
  608. package/templates/saas/lib/site.config.ts +200 -463
  609. package/templates/saas/lib/tasks.ts +101 -0
  610. package/templates/saas/lib/use-live.ts +25 -0
  611. package/templates/saas/public/favicon.ico +0 -0
  612. package/templates/saas/public/screenshots/board.png +0 -0
  613. package/templates/saas/public/screenshots/members.png +0 -0
  614. package/templates/saas/public/screenshots/overview.png +0 -0
  615. package/templates/saas/public/screenshots/projects.png +0 -0
  616. package/templates/saas/scripts/capture-screenshots.mjs +156 -0
  617. package/templates/saas/tests/names.test.ts +35 -0
  618. package/templates/saas/tests/plans.test.ts +12 -0
  619. package/templates/saas/tests/sample-workspace.test.ts +46 -0
  620. package/templates/saas/tests/tasks.test.ts +81 -0
  621. package/templates/shop/README.md +25 -13
  622. package/templates/shop/app/(marketing)/layout.tsx +94 -67
  623. package/templates/shop/app/(marketing)/not-found.tsx +13 -12
  624. package/templates/shop/app/(marketing)/page.tsx +120 -59
  625. package/templates/shop/app/(marketing)/shop-client.tsx +364 -237
  626. package/templates/shop/app/(marketing)/success/page.tsx +75 -22
  627. package/templates/shop/app/globals.css +94 -1
  628. package/templates/shop/app/layout.tsx +4 -4
  629. package/templates/shop/app/login/page.tsx +4 -4
  630. package/templates/shop/app.ts +18 -6
  631. package/templates/shop/components/cart-button.tsx +33 -0
  632. package/templates/shop/components/marketing.tsx +17 -87
  633. package/templates/shop/components/shelf-count.tsx +36 -0
  634. package/templates/shop/functions/checkout.ts +17 -1
  635. package/templates/shop/lib/cart-store.ts +57 -0
  636. package/templates/shop/lib/shelf-store.ts +35 -0
  637. package/templates/shop/lib/shop.ts +27 -0
  638. package/templates/shop/lib/site.config.ts +108 -98
  639. package/templates/shop/public/images/hero.jpg +0 -0
  640. package/templates/shop/public/images/products/cedar-smoke.jpg +0 -0
  641. package/templates/shop/public/images/products/fig-leaf.jpg +0 -0
  642. package/templates/shop/public/images/products/linen.jpg +0 -0
  643. package/templates/shop/public/images/products/sea-salt.jpg +0 -0
  644. package/templates/shop/public/images/products/tobacco-oak.jpg +0 -0
  645. package/templates/shop/tests/shop.test.ts +64 -0
  646. package/templates/waitlist/.env.example +7 -0
  647. package/templates/waitlist/README.md +29 -13
  648. package/templates/waitlist/app/(marketing)/page.tsx +9 -7
  649. package/templates/waitlist/app/(marketing)/waitlist-hero.tsx +64 -31
  650. package/templates/waitlist/app/auth-form.tsx +8 -8
  651. package/templates/waitlist/app/dashboard/dashboard-client.tsx +132 -95
  652. package/templates/waitlist/app/dashboard/page.tsx +17 -16
  653. package/templates/waitlist/app/icon.svg +2 -0
  654. package/templates/waitlist/app/login/page.tsx +14 -14
  655. package/templates/waitlist/app.ts +6 -6
  656. package/templates/waitlist/functions/joinWaitlist.ts +6 -18
  657. package/templates/waitlist/functions/seedDemo.ts +32 -0
  658. package/templates/waitlist/lib/demo.ts +94 -0
  659. package/templates/waitlist/lib/launch.ts +19 -0
  660. package/templates/waitlist/lib/site.config.ts +14 -9
  661. package/templates/waitlist/lib/stats.ts +35 -0
  662. package/templates/waitlist/public/favicon.ico +0 -0
  663. package/templates/waitlist/tests/counter.test.ts +107 -0
  664. package/templates/waitlist/tests/live-count.test.tsx +48 -0
  665. package/templates/ai-chat/tests/example.test.ts +0 -12
  666. package/templates/backend/consumer/apps/api/functions/feed.ts +0 -57
  667. package/templates/backend/consumer/apps/api/functions/profilePosts.ts +0 -17
  668. package/templates/backend/consumer/apps/api/functions/toggleLike.ts +0 -48
  669. package/templates/backend/consumer/apps/api/schema.ts +0 -143
  670. package/templates/chat/functions/_keep.ts +0 -13
  671. package/templates/chat/tests/example.test.ts +0 -12
  672. package/templates/ios/consumer/apps/ios/Sources/__APP_NAME_PASCAL__/ProfileSetupView.swift +0 -68
  673. package/templates/marketplace/client/LiveTicker.tsx +0 -116
  674. package/templates/marketplace/client/ScrollToListingsLink.tsx +0 -29
  675. package/templates/marketplace/public/images/marketplace-hero.webp +0 -0
@@ -0,0 +1,271 @@
1
+ import SwiftUI
2
+ import UIKit
3
+ import PylonClient
4
+
5
+ /// Image URLs and photo uploads against the Pylon server.
6
+ ///
7
+ /// Post and avatar URLs are server paths: `/images/...` for the demo photos
8
+ /// in apps/api/public, `/api/files/<id>` for uploads.
9
+ struct MediaService: Sendable {
10
+ let baseURL: URL
11
+ let client: PylonClient
12
+
13
+ /// The absolute URL for a stored image path.
14
+ func url(for path: String?) -> URL? {
15
+ guard let path, !path.isEmpty else { return nil }
16
+ if path.hasPrefix("http://") || path.hasPrefix("https://") { return URL(string: path) }
17
+ return URL(string: path, relativeTo: baseURL)?.absoluteURL
18
+ }
19
+
20
+ /// A resized copy for grids and avatars. The server's image optimizer
21
+ /// resizes files under public/. Uploads load at full size.
22
+ func thumbnailURL(for path: String?, width: Int) -> URL? {
23
+ guard let path, path.hasPrefix("/images/") else { return url(for: path) }
24
+ var components = URLComponents(url: baseURL.appendingPathComponent("_pylon/image"), resolvingAgainstBaseURL: false)
25
+ components?.queryItems = [
26
+ URLQueryItem(name: "src", value: path),
27
+ URLQueryItem(name: "w", value: String(width)),
28
+ URLQueryItem(name: "q", value: "75"),
29
+ URLQueryItem(name: "format", value: "jpeg"),
30
+ ]
31
+ return components?.url
32
+ }
33
+
34
+ /// Upload a JPEG as a public file and return its `/api/files/<id>` path.
35
+ ///
36
+ /// Pylon's upload is three requests: `POST /api/files/init` reserves a
37
+ /// slot, the bytes go to the returned `uploadUrl` with `PUT`, and
38
+ /// `POST /api/files/confirm` finishes it. `visibility: "public"` lets
39
+ /// every viewer load the photo without a session.
40
+ func uploadPublicJPEG(_ data: Data) async throws -> String {
41
+ struct InitBody: Encodable {
42
+ let filename: String
43
+ let mimeType: String
44
+ let size: Int
45
+ let visibility: String
46
+ }
47
+ struct InitResponse: Decodable {
48
+ let uploadUrl: String
49
+ let assetId: String
50
+ }
51
+ struct ConfirmBody: Encodable { let assetId: String }
52
+ struct ConfirmResponse: Decodable { let id: String }
53
+
54
+ let token = await client.currentToken()
55
+ let slot: InitResponse = try await send(
56
+ "POST",
57
+ to: baseURL.appendingPathComponent("api/files/init"),
58
+ json: InitBody(filename: "photo.jpg", mimeType: "image/jpeg", size: data.count, visibility: "public"),
59
+ token: token
60
+ )
61
+
62
+ guard let target = URL(string: slot.uploadUrl, relativeTo: baseURL)?.absoluteURL else {
63
+ throw MediaError.badResponse
64
+ }
65
+ var put = URLRequest(url: target)
66
+ put.httpMethod = "PUT"
67
+ put.setValue("image/jpeg", forHTTPHeaderField: "Content-Type")
68
+ // Only this server gets the session token. A storage provider's
69
+ // presigned URL carries its own authorization.
70
+ if target.host == baseURL.host, target.port == baseURL.port, let token {
71
+ put.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization")
72
+ }
73
+ let (_, putResponse) = try await URLSession.shared.upload(for: put, from: data)
74
+ guard let http = putResponse as? HTTPURLResponse, (200..<300).contains(http.statusCode) else {
75
+ throw MediaError.uploadFailed
76
+ }
77
+
78
+ let confirmed: ConfirmResponse = try await send(
79
+ "POST",
80
+ to: baseURL.appendingPathComponent("api/files/confirm"),
81
+ json: ConfirmBody(assetId: slot.assetId),
82
+ token: token
83
+ )
84
+ // Store the server path. With S3 or Stack0 storage the confirm
85
+ // response's `url` is a CDN address; `/api/files/<id>` works for every
86
+ // backend and redirects to the CDN when there is one.
87
+ return "/api/files/\(confirmed.id)"
88
+ }
89
+
90
+ private func send<Body: Encodable, Response: Decodable>(
91
+ _ method: String,
92
+ to url: URL,
93
+ json body: Body,
94
+ token: String?
95
+ ) async throws -> Response {
96
+ var request = URLRequest(url: url)
97
+ request.httpMethod = method
98
+ request.setValue("application/json", forHTTPHeaderField: "Content-Type")
99
+ if let token { request.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization") }
100
+ request.httpBody = try JSONEncoder().encode(body)
101
+ let (data, response) = try await URLSession.shared.data(for: request)
102
+ guard let http = response as? HTTPURLResponse else { throw MediaError.uploadFailed }
103
+ guard (200..<300).contains(http.statusCode) else {
104
+ let inner = (try? JSONDecoder().decode(ServerErrorBody.self, from: data))?.error
105
+ throw PylonError.http(status: http.statusCode, code: inner?.code, message: inner?.message)
106
+ }
107
+ return try JSONDecoder().decode(Response.self, from: data)
108
+ }
109
+ }
110
+
111
+ /// Pylon's error body: `{"error": {"code", "message"}}`.
112
+ private struct ServerErrorBody: Decodable {
113
+ struct Inner: Decodable {
114
+ let code: String?
115
+ let message: String?
116
+ }
117
+ let error: Inner?
118
+ }
119
+
120
+ enum MediaError: LocalizedError {
121
+ case badResponse
122
+ case uploadFailed
123
+ case unreadableImage
124
+
125
+ var errorDescription: String? {
126
+ switch self {
127
+ case .badResponse, .uploadFailed: "The photo did not upload. Try again."
128
+ case .unreadableImage: "This photo cannot be read. Choose another one."
129
+ }
130
+ }
131
+ }
132
+
133
+ extension UIImage {
134
+ /// A JPEG no wider or taller than `maxPixels`, for upload.
135
+ func jpegForUpload(maxPixels: CGFloat = 1600, quality: CGFloat = 0.82) -> Data? {
136
+ let longest = max(size.width, size.height)
137
+ let scale = min(1, maxPixels / max(longest, 1))
138
+ let target = CGSize(width: (size.width * scale).rounded(), height: (size.height * scale).rounded())
139
+ let format = UIGraphicsImageRendererFormat()
140
+ format.scale = 1
141
+ let resized = UIGraphicsImageRenderer(size: target, format: format).image { _ in
142
+ draw(in: CGRect(origin: .zero, size: target))
143
+ }
144
+ return resized.jpegData(compressionQuality: quality)
145
+ }
146
+ }
147
+
148
+ // MARK: - Image loading
149
+
150
+ /// Loads and caches decoded images. Rows scrolled off and back on screen
151
+ /// draw from memory; the URL cache keeps the bytes on disk between launches.
152
+ actor ImagePipeline {
153
+ static let shared = ImagePipeline()
154
+
155
+ nonisolated let memory = MemoryCache()
156
+ private let session: URLSession = {
157
+ let config = URLSessionConfiguration.default
158
+ config.urlCache = URLCache(memoryCapacity: 32 * 1024 * 1024, diskCapacity: 300 * 1024 * 1024)
159
+ config.requestCachePolicy = .returnCacheDataElseLoad
160
+ return URLSession(configuration: config)
161
+ }()
162
+ private var inFlight: [URL: Task<UIImage?, Never>] = [:]
163
+
164
+ nonisolated func cached(_ url: URL) -> UIImage? {
165
+ memory.image(for: url)
166
+ }
167
+
168
+ func image(for url: URL) async -> UIImage? {
169
+ if let hit = cached(url) { return hit }
170
+ if let running = inFlight[url] { return await running.value }
171
+ let session = self.session
172
+ let task = Task<UIImage?, Never> {
173
+ guard let (data, _) = try? await session.data(from: url),
174
+ let image = UIImage(data: data) else { return nil }
175
+ return await image.byPreparingForDisplay() ?? image
176
+ }
177
+ inFlight[url] = task
178
+ let image = await task.value
179
+ inFlight[url] = nil
180
+ if let image {
181
+ let cost = Int(image.size.width * image.size.height * image.scale * image.scale * 4)
182
+ memory.store(image, for: url, cost: cost)
183
+ }
184
+ return image
185
+ }
186
+ }
187
+
188
+ /// Decoded images kept in memory. NSCache is safe to use from any thread.
189
+ final class MemoryCache: @unchecked Sendable {
190
+ private let cache: NSCache<NSURL, UIImage> = {
191
+ let cache = NSCache<NSURL, UIImage>()
192
+ cache.totalCostLimit = 150 * 1024 * 1024
193
+ return cache
194
+ }()
195
+
196
+ func image(for url: URL) -> UIImage? { cache.object(forKey: url as NSURL) }
197
+
198
+ func store(_ image: UIImage, for url: URL, cost: Int) {
199
+ cache.setObject(image, forKey: url as NSURL, cost: cost)
200
+ }
201
+ }
202
+
203
+ /// A remote photo that fills its frame. Shows a neutral placeholder while it loads.
204
+ struct RemoteImage: View {
205
+ let url: URL?
206
+ var placeholder: Color = Color(uiColor: .secondarySystemBackground)
207
+ @State private var image: UIImage?
208
+
209
+ init(url: URL?, placeholder: Color = Color(uiColor: .secondarySystemBackground)) {
210
+ self.url = url
211
+ self.placeholder = placeholder
212
+ _image = State(initialValue: url.flatMap { ImagePipeline.shared.cached($0) })
213
+ }
214
+
215
+ var body: some View {
216
+ Rectangle()
217
+ .fill(placeholder)
218
+ .overlay {
219
+ if let image {
220
+ Image(uiImage: image)
221
+ .resizable()
222
+ .scaledToFill()
223
+ .transition(.opacity)
224
+ }
225
+ }
226
+ .clipped()
227
+ .task(id: url) {
228
+ guard let url else { image = nil; return }
229
+ if let hit = ImagePipeline.shared.cached(url) {
230
+ image = hit
231
+ return
232
+ }
233
+ let loaded = await ImagePipeline.shared.image(for: url)
234
+ withAnimation(.easeOut(duration: 0.2)) { image = loaded }
235
+ }
236
+ }
237
+ }
238
+
239
+ /// A round profile photo over the first letter of the name on a tinted
240
+ /// circle. The letter shows while the photo loads, or when there is none.
241
+ struct AvatarView: View {
242
+ @EnvironmentObject private var social: SocialStore
243
+ let profile: Profile?
244
+ var size: CGFloat = 32
245
+
246
+ var body: some View {
247
+ ZStack {
248
+ Circle().fill(tint.gradient)
249
+ Text(initial)
250
+ .font(.system(size: size * 0.42, weight: .semibold, design: .rounded))
251
+ .foregroundStyle(.white)
252
+ if let path = profile?.avatarUrl, !path.isEmpty {
253
+ RemoteImage(url: social.media.thumbnailURL(for: path, width: size > 64 ? 256 : 128), placeholder: .clear)
254
+ }
255
+ }
256
+ .frame(width: size, height: size)
257
+ .clipShape(Circle())
258
+ .accessibilityHidden(true)
259
+ }
260
+
261
+ private var initial: String {
262
+ String((profile?.displayName ?? profile?.handle ?? "?").prefix(1)).uppercased()
263
+ }
264
+
265
+ private var tint: Color {
266
+ let palette: [Color] = [.orange, .pink, .purple, .indigo, .teal, .green, .blue, .red]
267
+ let key = profile?.handle ?? ""
268
+ let index = key.unicodeScalars.reduce(0) { ($0 &* 31 &+ Int($1.value)) & 0xFFFF } % palette.count
269
+ return palette[index]
270
+ }
271
+ }
@@ -1,19 +1,26 @@
1
1
  import Foundation
2
2
 
3
+ // Row shapes for the entities in apps/api/app.ts. The sync engine hands
4
+ // every row over as JSON, so optional fields must be optional here too.
5
+
3
6
  struct Profile: Codable, Identifiable, Hashable {
4
7
  let id: String
5
8
  let userId: String
6
9
  let handle: String
7
10
  let displayName: String
8
11
  let bio: String?
12
+ let avatarUrl: String?
9
13
  let createdAt: String
10
14
  }
11
15
 
12
16
  struct Post: Codable, Identifiable, Hashable {
13
17
  let id: String
14
18
  let authorId: String
15
- let body: String
19
+ let imageUrl: String
20
+ let caption: String?
16
21
  let createdAt: String
22
+
23
+ var date: Date { Timestamp.parse(createdAt) }
17
24
  }
18
25
 
19
26
  struct Like: Codable, Identifiable, Hashable {
@@ -23,12 +30,102 @@ struct Like: Codable, Identifiable, Hashable {
23
30
  let createdAt: String
24
31
  }
25
32
 
33
+ struct Comment: Codable, Identifiable, Hashable {
34
+ let id: String
35
+ let postId: String
36
+ let profileId: String
37
+ let text: String
38
+ let createdAt: String
39
+
40
+ var date: Date { Timestamp.parse(createdAt) }
41
+ }
42
+
43
+ struct Follow: Codable, Identifiable, Hashable {
44
+ let id: String
45
+ let followerId: String
46
+ let followingId: String
47
+ let createdAt: String
48
+ }
49
+
50
+ // MARK: - Function arguments and results
51
+
52
+ struct EmptyArgs: Encodable {}
53
+ struct SeedResult: Decodable { let seeded: Bool }
54
+ struct IdResult: Decodable { let id: String }
26
55
  struct UpsertProfileArgs: Encodable {
27
56
  let handle: String
28
57
  let displayName: String
29
58
  let bio: String
59
+ let avatarUrl: String?
60
+ }
61
+ struct CreatePostArgs: Encodable {
62
+ let imageUrl: String
63
+ let caption: String
64
+ }
65
+ struct SetLikeArgs: Encodable {
66
+ let postId: String
67
+ let liked: Bool
30
68
  }
31
- struct CreatePostArgs: Encodable { let body: String }
32
69
  struct DeletePostArgs: Encodable { let id: String }
33
- struct ToggleLikeArgs: Encodable { let postId: String }
34
- struct EmptyArgs: Encodable {}
70
+ struct AddCommentArgs: Encodable {
71
+ let postId: String
72
+ let text: String
73
+ }
74
+ struct SetFollowArgs: Encodable {
75
+ let profileId: String
76
+ let following: Bool
77
+ }
78
+ struct SetLikeResult: Decodable {
79
+ let liked: Bool
80
+ let likeCount: Int
81
+ }
82
+ struct SetFollowResult: Decodable { let following: Bool }
83
+
84
+ // MARK: - Timestamps
85
+
86
+ enum Timestamp {
87
+ private static let withFraction: ISO8601DateFormatter = {
88
+ let f = ISO8601DateFormatter()
89
+ f.formatOptions = [.withInternetDateTime, .withFractionalSeconds]
90
+ return f
91
+ }()
92
+ private static let plain = ISO8601DateFormatter()
93
+
94
+ static func parse(_ iso: String) -> Date {
95
+ withFraction.date(from: iso) ?? plain.date(from: iso) ?? .distantPast
96
+ }
97
+
98
+ /// "now", "5m", "2h", "3d", "2w", then "Sep 3".
99
+ static func short(_ date: Date, now: Date = .now) -> String {
100
+ let seconds = max(0, now.timeIntervalSince(date))
101
+ switch seconds {
102
+ case ..<60: return "now"
103
+ case ..<3_600: return "\(Int(seconds / 60))m"
104
+ case ..<86_400: return "\(Int(seconds / 3_600))h"
105
+ case ..<604_800: return "\(Int(seconds / 86_400))d"
106
+ case ..<2_419_200: return "\(Int(seconds / 604_800))w"
107
+ default: return date.formatted(.dateTime.month(.abbreviated).day())
108
+ }
109
+ }
110
+
111
+ /// "Just now", "5 minutes ago", "2 hours ago", "3 days ago", then "September 3".
112
+ static func long(_ date: Date, now: Date = .now) -> String {
113
+ let seconds = max(0, now.timeIntervalSince(date))
114
+ func unit(_ n: Int, _ name: String) -> String { "\(n) \(name)\(n == 1 ? "" : "s") ago" }
115
+ switch seconds {
116
+ case ..<60: return "Just now"
117
+ case ..<3_600: return unit(Int(seconds / 60), "minute")
118
+ case ..<86_400: return unit(Int(seconds / 3_600), "hour")
119
+ case ..<604_800: return unit(Int(seconds / 86_400), "day")
120
+ default: return date.formatted(.dateTime.month(.wide).day())
121
+ }
122
+ }
123
+ }
124
+
125
+ extension Int {
126
+ /// 950 → "950", 1_204 → "1,204", 12_400 → "12.4K".
127
+ var compactCount: String {
128
+ if self < 10_000 { return formatted(.number) }
129
+ return formatted(.number.notation(.compactName).precision(.fractionLength(0...1)))
130
+ }
131
+ }
@@ -0,0 +1,245 @@
1
+ import SwiftUI
2
+
3
+ /// Pushed screens, shared by every tab's NavigationStack.
4
+ enum Route: Hashable {
5
+ case profile(String)
6
+ case post(String)
7
+ case profilePosts(profileId: String, startAt: String)
8
+ }
9
+
10
+ extension View {
11
+ /// Registers the destinations for `Route` values on a NavigationStack.
12
+ func appRoutes() -> some View {
13
+ navigationDestination(for: Route.self) { route in
14
+ switch route {
15
+ case .profile(let id): ProfileView(profileId: id, isRoot: false)
16
+ case .post(let id): PostDetailView(postId: id)
17
+ case .profilePosts(let profileId, let startAt): ProfilePostsView(profileId: profileId, startAt: startAt)
18
+ }
19
+ }
20
+ }
21
+ }
22
+
23
+ /// One post in a feed: author, photo, actions, likes, caption, comments.
24
+ struct PostCard: View {
25
+ @EnvironmentObject private var social: SocialStore
26
+ let post: Post
27
+
28
+ @State private var showBurst = false
29
+ @State private var burstTrigger = 0
30
+ @State private var showComments = false
31
+ @State private var confirmDelete = false
32
+
33
+ private var author: Profile? { social.profile(post.authorId) }
34
+ private var liked: Bool { social.isLiked(post.id) }
35
+ private var likeCount: Int { social.likeCount(post.id) }
36
+ private var comments: [Comment] { social.comments(post.id) }
37
+
38
+ var body: some View {
39
+ VStack(alignment: .leading, spacing: 0) {
40
+ header
41
+ photo
42
+ actions
43
+ details
44
+ }
45
+ .padding(.bottom, 14)
46
+ .sheet(isPresented: $showComments) {
47
+ CommentsView(postId: post.id)
48
+ .environmentObject(social)
49
+ .presentationDetents([.medium, .large])
50
+ .presentationDragIndicator(.visible)
51
+ }
52
+ .confirmationDialog("Delete this post?", isPresented: $confirmDelete, titleVisibility: .visible) {
53
+ Button("Delete", role: .destructive) {
54
+ Task { try? await social.deletePost(post.id) }
55
+ }
56
+ } message: {
57
+ Text("The photo, its likes, and its comments are removed for everyone.")
58
+ }
59
+ }
60
+
61
+ private var header: some View {
62
+ HStack(spacing: 10) {
63
+ NavigationLink(value: Route.profile(post.authorId)) {
64
+ HStack(spacing: 10) {
65
+ StoryAvatar(profile: author, size: 34, ringed: social.hasRecentPost(post.authorId))
66
+ HStack(spacing: 5) {
67
+ Text(author?.handle ?? "")
68
+ .font(.subheadline.weight(.semibold))
69
+ Text(Timestamp.short(post.date))
70
+ .font(.subheadline)
71
+ .foregroundStyle(.secondary)
72
+ }
73
+ }
74
+ }
75
+ .buttonStyle(.plain)
76
+ Spacer()
77
+ Menu {
78
+ if let url = social.media.url(for: post.imageUrl) {
79
+ ShareLink(item: url) { Label("Share", systemImage: "square.and.arrow.up") }
80
+ }
81
+ if post.authorId == social.myProfileId {
82
+ Button(role: .destructive) { confirmDelete = true } label: {
83
+ Label("Delete", systemImage: "trash")
84
+ }
85
+ }
86
+ } label: {
87
+ Image(systemName: "ellipsis")
88
+ .font(.system(size: 16, weight: .semibold))
89
+ .frame(width: 36, height: 36)
90
+ .contentShape(Rectangle())
91
+ }
92
+ .foregroundStyle(.primary)
93
+ .accessibilityLabel("More")
94
+ }
95
+ .padding(.leading, 12)
96
+ .padding(.trailing, 4)
97
+ .padding(.vertical, 8)
98
+ }
99
+
100
+ private var photo: some View {
101
+ Color.clear
102
+ .aspectRatio(4 / 5, contentMode: .fit)
103
+ .overlay { RemoteImage(url: social.media.url(for: post.imageUrl)) }
104
+ .clipped()
105
+ .overlay {
106
+ Image(systemName: "heart.fill")
107
+ .font(.system(size: 100))
108
+ .foregroundStyle(.white)
109
+ .shadow(color: .black.opacity(0.25), radius: 12, y: 4)
110
+ .scaleEffect(showBurst ? 1 : 0.3)
111
+ .opacity(showBurst ? 1 : 0)
112
+ .allowsHitTesting(false)
113
+ }
114
+ .contentShape(Rectangle())
115
+ .onTapGesture(count: 2) {
116
+ social.like(post.id)
117
+ burstTrigger += 1
118
+ withAnimation(.spring(response: 0.28, dampingFraction: 0.5)) { showBurst = true }
119
+ Task {
120
+ try? await Task.sleep(for: .milliseconds(650))
121
+ withAnimation(.easeIn(duration: 0.2)) { showBurst = false }
122
+ }
123
+ }
124
+ .sensoryFeedback(.impact(weight: .light), trigger: burstTrigger)
125
+ .accessibilityLabel(post.caption ?? "Photo")
126
+ .accessibilityAddTraits(.isImage)
127
+ }
128
+
129
+ private var actions: some View {
130
+ HStack(spacing: 18) {
131
+ Button {
132
+ social.toggleLike(post.id)
133
+ } label: {
134
+ Image(systemName: liked ? "heart.fill" : "heart")
135
+ .foregroundStyle(liked ? Theme.heart : .primary)
136
+ .symbolEffect(.bounce, value: liked)
137
+ }
138
+ .accessibilityLabel(liked ? "Unlike" : "Like")
139
+ .sensoryFeedback(.selection, trigger: liked)
140
+
141
+ Button { showComments = true } label: {
142
+ Image(systemName: "bubble.right")
143
+ }
144
+ .accessibilityLabel("Comment")
145
+
146
+ if let url = social.media.url(for: post.imageUrl) {
147
+ ShareLink(item: url) {
148
+ Image(systemName: "paperplane")
149
+ }
150
+ .accessibilityLabel("Share")
151
+ }
152
+ Spacer()
153
+ }
154
+ .font(.system(size: 23))
155
+ .foregroundStyle(.primary)
156
+ .buttonStyle(.plain)
157
+ .padding(.horizontal, 14)
158
+ .padding(.top, 12)
159
+ .padding(.bottom, 8)
160
+ }
161
+
162
+ private var details: some View {
163
+ VStack(alignment: .leading, spacing: 5) {
164
+ likesLine
165
+ if let caption = post.caption, !caption.isEmpty {
166
+ (Text(author?.handle ?? "").fontWeight(.semibold) + Text(" ") + Text(caption))
167
+ .font(.subheadline)
168
+ .lineLimit(3)
169
+ }
170
+ if !comments.isEmpty {
171
+ Button {
172
+ showComments = true
173
+ } label: {
174
+ Text(comments.count == 1 ? "View 1 comment" : "View all \(comments.count) comments")
175
+ .font(.subheadline)
176
+ .foregroundStyle(.secondary)
177
+ }
178
+ .buttonStyle(.plain)
179
+ }
180
+ }
181
+ .padding(.horizontal, 14)
182
+ }
183
+
184
+ @ViewBuilder
185
+ private var likesLine: some View {
186
+ if likeCount > 0 {
187
+ if let liker = social.featuredLiker(post.id), likeCount > 1 {
188
+ HStack(spacing: 6) {
189
+ AvatarView(profile: liker, size: 18)
190
+ (Text("Liked by ") + Text(liker.handle).fontWeight(.semibold)
191
+ + Text(" and ") + Text(likeCount - 1 == 1 ? "1 other" : "\((likeCount - 1).compactCount) others").fontWeight(.semibold))
192
+ .font(.subheadline)
193
+ .lineLimit(1)
194
+ }
195
+ .contentTransition(.numericText())
196
+ } else {
197
+ Text(likeCount == 1 ? "1 like" : "\(likeCount.compactCount) likes")
198
+ .font(.subheadline.weight(.semibold))
199
+ .contentTransition(.numericText())
200
+ }
201
+ }
202
+ }
203
+ }
204
+
205
+ /// An avatar with the gradient ring for someone who posted in the last 24 hours.
206
+ struct StoryAvatar: View {
207
+ let profile: Profile?
208
+ var size: CGFloat
209
+ var ringed: Bool
210
+
211
+ var body: some View {
212
+ let ring = max(2, size * 0.045)
213
+ let gap = max(2, size * 0.05)
214
+ AvatarView(profile: profile, size: size)
215
+ .padding(gap)
216
+ .background(Circle().fill(Color(uiColor: .systemBackground)))
217
+ .padding(ring)
218
+ .background {
219
+ if ringed {
220
+ Circle().fill(Theme.storyRing)
221
+ } else {
222
+ Circle().stroke(Theme.hairline, lineWidth: 0.5).padding(ring)
223
+ }
224
+ }
225
+ }
226
+ }
227
+
228
+ /// A single post on its own screen, reached from grids and activity.
229
+ struct PostDetailView: View {
230
+ @EnvironmentObject private var social: SocialStore
231
+ let postId: String
232
+
233
+ var body: some View {
234
+ ScrollView {
235
+ if let post = social.post(postId) {
236
+ PostCard(post: post)
237
+ } else {
238
+ ContentUnavailableView("Post removed", systemImage: "photo", description: Text("This post is no longer available."))
239
+ .padding(.top, 80)
240
+ }
241
+ }
242
+ .navigationTitle("Post")
243
+ .navigationBarTitleDisplayMode(.inline)
244
+ }
245
+ }