@pylonsync/create-pylon 0.3.274 → 0.3.276

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 (340) hide show
  1. package/bin/create-pylon.js +80 -0
  2. package/package.json +1 -1
  3. package/templates/ARCHETYPES.md +339 -0
  4. package/templates/agency/.env.example +12 -0
  5. package/templates/agency/AGENTS.md +61 -0
  6. package/templates/agency/README.md +90 -0
  7. package/templates/agency/app/auth-form.tsx +129 -0
  8. package/templates/agency/app/contact-form.tsx +258 -0
  9. package/templates/agency/app/dashboard/dashboard-client.tsx +1440 -0
  10. package/templates/agency/app/dashboard/page.tsx +70 -0
  11. package/templates/agency/app/error.tsx +26 -0
  12. package/templates/agency/app/globals.css +148 -0
  13. package/templates/agency/app/layout.tsx +174 -0
  14. package/templates/agency/app/login/page.tsx +39 -0
  15. package/templates/agency/app/not-found.tsx +19 -0
  16. package/templates/agency/app/page.tsx +249 -0
  17. package/templates/agency/app/robots.ts +12 -0
  18. package/templates/agency/app/seeder.tsx +26 -0
  19. package/templates/agency/app/sitemap.ts +9 -0
  20. package/templates/agency/app/work/[slug]/page.tsx +182 -0
  21. package/templates/agency/app/work/page.tsx +83 -0
  22. package/templates/agency/app.ts +284 -0
  23. package/templates/agency/components/marketing.tsx +187 -0
  24. package/templates/agency/components/section-scroller.tsx +35 -0
  25. package/templates/agency/components/ui/button.tsx +56 -0
  26. package/templates/agency/components/ui/card.tsx +90 -0
  27. package/templates/agency/components.json +20 -0
  28. package/templates/agency/functions/bookInquiry.ts +42 -0
  29. package/templates/agency/functions/clientsForOwner.ts +27 -0
  30. package/templates/agency/functions/declineInquiry.ts +41 -0
  31. package/templates/agency/functions/deleteClient.ts +27 -0
  32. package/templates/agency/functions/deleteInvoice.ts +19 -0
  33. package/templates/agency/functions/deleteProject.ts +20 -0
  34. package/templates/agency/functions/inquiriesForOwner.ts +31 -0
  35. package/templates/agency/functions/invoicesForOwner.ts +27 -0
  36. package/templates/agency/functions/seedCapacity.ts +26 -0
  37. package/templates/agency/functions/seedProjects.ts +41 -0
  38. package/templates/agency/functions/seedStudioBackoffice.ts +74 -0
  39. package/templates/agency/functions/setCapacity.ts +32 -0
  40. package/templates/agency/functions/setInvoiceStatus.ts +27 -0
  41. package/templates/agency/functions/setProjectFlags.ts +35 -0
  42. package/templates/agency/functions/submitInquiry.ts +55 -0
  43. package/templates/agency/functions/upsertClient.ts +73 -0
  44. package/templates/agency/functions/upsertInvoice.ts +113 -0
  45. package/templates/agency/functions/upsertProject.ts +97 -0
  46. package/templates/agency/gitignore +10 -0
  47. package/templates/agency/lib/agency.ts +189 -0
  48. package/templates/agency/lib/invoice-pdf.tsx +174 -0
  49. package/templates/agency/lib/owner.ts +26 -0
  50. package/templates/agency/lib/site.config.ts +418 -0
  51. package/templates/agency/lib/utils.ts +10 -0
  52. package/templates/agency/package.json +35 -0
  53. package/templates/agency/tsconfig.json +18 -0
  54. package/templates/ai-chat/.env.example +33 -0
  55. package/templates/ai-chat/AGENTS.md +61 -0
  56. package/templates/ai-chat/README.md +99 -0
  57. package/templates/ai-chat/app/auth-form.tsx +124 -0
  58. package/templates/ai-chat/app/chat-client.tsx +727 -0
  59. package/templates/ai-chat/app/error.tsx +26 -0
  60. package/templates/ai-chat/app/globals.css +148 -0
  61. package/templates/ai-chat/app/layout.tsx +75 -0
  62. package/templates/ai-chat/app/login/page.tsx +39 -0
  63. package/templates/ai-chat/app/not-found.tsx +19 -0
  64. package/templates/ai-chat/app/page.tsx +23 -0
  65. package/templates/ai-chat/app.ts +121 -0
  66. package/templates/ai-chat/components.json +20 -0
  67. package/templates/ai-chat/functions/deleteConversation.ts +33 -0
  68. package/templates/ai-chat/gitignore +10 -0
  69. package/templates/ai-chat/lib/site.config.ts +103 -0
  70. package/templates/ai-chat/lib/utils.ts +10 -0
  71. package/templates/ai-chat/package.json +34 -0
  72. package/templates/ai-chat/tsconfig.json +18 -0
  73. package/templates/ai-studio/.env.example +19 -0
  74. package/templates/ai-studio/AGENTS.md +61 -0
  75. package/templates/ai-studio/README.md +83 -0
  76. package/templates/ai-studio/app/auth-form.tsx +124 -0
  77. package/templates/ai-studio/app/error.tsx +26 -0
  78. package/templates/ai-studio/app/globals.css +148 -0
  79. package/templates/ai-studio/app/layout.tsx +75 -0
  80. package/templates/ai-studio/app/login/page.tsx +39 -0
  81. package/templates/ai-studio/app/not-found.tsx +19 -0
  82. package/templates/ai-studio/app/page.tsx +34 -0
  83. package/templates/ai-studio/app/studio-client.tsx +357 -0
  84. package/templates/ai-studio/app.ts +108 -0
  85. package/templates/ai-studio/components.json +20 -0
  86. package/templates/ai-studio/functions/_getGeneration.ts +25 -0
  87. package/templates/ai-studio/functions/_updateGeneration.ts +37 -0
  88. package/templates/ai-studio/functions/generate.ts +42 -0
  89. package/templates/ai-studio/functions/pollGeneration.ts +134 -0
  90. package/templates/ai-studio/gitignore +10 -0
  91. package/templates/ai-studio/lib/site.config.ts +80 -0
  92. package/templates/ai-studio/lib/studio.ts +52 -0
  93. package/templates/ai-studio/lib/utils.ts +10 -0
  94. package/templates/ai-studio/package.json +34 -0
  95. package/templates/ai-studio/tsconfig.json +18 -0
  96. package/templates/creator/.env.example +12 -0
  97. package/templates/creator/AGENTS.md +61 -0
  98. package/templates/creator/README.md +67 -0
  99. package/templates/creator/app/auth-form.tsx +129 -0
  100. package/templates/creator/app/dashboard/dashboard-client.tsx +297 -0
  101. package/templates/creator/app/dashboard/page.tsx +70 -0
  102. package/templates/creator/app/error.tsx +26 -0
  103. package/templates/creator/app/globals.css +148 -0
  104. package/templates/creator/app/layout.tsx +160 -0
  105. package/templates/creator/app/login/page.tsx +39 -0
  106. package/templates/creator/app/newsletter-signup.tsx +162 -0
  107. package/templates/creator/app/not-found.tsx +19 -0
  108. package/templates/creator/app/page.tsx +160 -0
  109. package/templates/creator/app/robots.ts +12 -0
  110. package/templates/creator/app/sitemap.ts +9 -0
  111. package/templates/creator/app.ts +134 -0
  112. package/templates/creator/components/marketing.tsx +148 -0
  113. package/templates/creator/components/section-scroller.tsx +35 -0
  114. package/templates/creator/components/ui/button.tsx +56 -0
  115. package/templates/creator/components/ui/card.tsx +90 -0
  116. package/templates/creator/components.json +20 -0
  117. package/templates/creator/functions/subscribe.ts +82 -0
  118. package/templates/creator/functions/subscriberStats.ts +75 -0
  119. package/templates/creator/gitignore +10 -0
  120. package/templates/creator/lib/owner.ts +26 -0
  121. package/templates/creator/lib/site.config.ts +173 -0
  122. package/templates/creator/lib/stats.ts +30 -0
  123. package/templates/creator/lib/utils.ts +10 -0
  124. package/templates/creator/package.json +34 -0
  125. package/templates/creator/tsconfig.json +18 -0
  126. package/templates/default/app/layout.tsx +26 -27
  127. package/templates/default/app/page.tsx +90 -274
  128. package/templates/default/lib/products.ts +9 -122
  129. package/templates/default/lib/site.config.ts +739 -0
  130. package/templates/default/lib/site.ts +14 -261
  131. package/templates/directory/.env.example +12 -0
  132. package/templates/directory/AGENTS.md +61 -0
  133. package/templates/directory/README.md +80 -0
  134. package/templates/directory/app/auth-form.tsx +129 -0
  135. package/templates/directory/app/dashboard/dashboard-client.tsx +205 -0
  136. package/templates/directory/app/dashboard/page.tsx +70 -0
  137. package/templates/directory/app/directory-browse.tsx +328 -0
  138. package/templates/directory/app/error.tsx +26 -0
  139. package/templates/directory/app/globals.css +148 -0
  140. package/templates/directory/app/layout.tsx +171 -0
  141. package/templates/directory/app/login/page.tsx +39 -0
  142. package/templates/directory/app/not-found.tsx +19 -0
  143. package/templates/directory/app/page.tsx +50 -0
  144. package/templates/directory/app/robots.ts +12 -0
  145. package/templates/directory/app/sitemap.ts +9 -0
  146. package/templates/directory/app/submit/page.tsx +30 -0
  147. package/templates/directory/app/submit-form.tsx +151 -0
  148. package/templates/directory/app.ts +146 -0
  149. package/templates/directory/components/marketing.tsx +148 -0
  150. package/templates/directory/components/section-scroller.tsx +35 -0
  151. package/templates/directory/components/ui/button.tsx +56 -0
  152. package/templates/directory/components/ui/card.tsx +90 -0
  153. package/templates/directory/components.json +20 -0
  154. package/templates/directory/functions/approveSubmission.ts +45 -0
  155. package/templates/directory/functions/rejectSubmission.ts +20 -0
  156. package/templates/directory/functions/seedListings.ts +33 -0
  157. package/templates/directory/functions/submissionsForOwner.ts +29 -0
  158. package/templates/directory/functions/submitListing.ts +63 -0
  159. package/templates/directory/functions/upvote.ts +24 -0
  160. package/templates/directory/gitignore +10 -0
  161. package/templates/directory/lib/directory.ts +45 -0
  162. package/templates/directory/lib/owner.ts +26 -0
  163. package/templates/directory/lib/site.config.ts +130 -0
  164. package/templates/directory/lib/utils.ts +10 -0
  165. package/templates/directory/package.json +34 -0
  166. package/templates/directory/tsconfig.json +18 -0
  167. package/templates/local-service/.env.example +12 -0
  168. package/templates/local-service/AGENTS.md +61 -0
  169. package/templates/local-service/README.md +82 -0
  170. package/templates/local-service/app/auth-form.tsx +129 -0
  171. package/templates/local-service/app/booking-widget.tsx +399 -0
  172. package/templates/local-service/app/dashboard/dashboard-client.tsx +304 -0
  173. package/templates/local-service/app/dashboard/page.tsx +63 -0
  174. package/templates/local-service/app/error.tsx +26 -0
  175. package/templates/local-service/app/globals.css +148 -0
  176. package/templates/local-service/app/layout.tsx +151 -0
  177. package/templates/local-service/app/login/page.tsx +39 -0
  178. package/templates/local-service/app/not-found.tsx +19 -0
  179. package/templates/local-service/app/page.tsx +233 -0
  180. package/templates/local-service/app/robots.ts +12 -0
  181. package/templates/local-service/app/sitemap.ts +9 -0
  182. package/templates/local-service/app.ts +131 -0
  183. package/templates/local-service/components/marketing.tsx +162 -0
  184. package/templates/local-service/components/section-scroller.tsx +35 -0
  185. package/templates/local-service/components/ui/button.tsx +56 -0
  186. package/templates/local-service/components/ui/card.tsx +90 -0
  187. package/templates/local-service/components.json +20 -0
  188. package/templates/local-service/functions/bookingsForOwner.ts +30 -0
  189. package/templates/local-service/functions/cancelBooking.ts +27 -0
  190. package/templates/local-service/functions/confirmBooking.ts +18 -0
  191. package/templates/local-service/functions/createBooking.ts +98 -0
  192. package/templates/local-service/gitignore +10 -0
  193. package/templates/local-service/lib/booking.ts +24 -0
  194. package/templates/local-service/lib/owner.ts +26 -0
  195. package/templates/local-service/lib/site.config.ts +232 -0
  196. package/templates/local-service/lib/slots.ts +97 -0
  197. package/templates/local-service/lib/utils.ts +10 -0
  198. package/templates/local-service/package.json +34 -0
  199. package/templates/local-service/tsconfig.json +18 -0
  200. package/templates/marketplace/.env.example +9 -0
  201. package/templates/marketplace/AGENTS.md +61 -0
  202. package/templates/marketplace/README.md +78 -0
  203. package/templates/marketplace/app/_components/CategoryIcon.tsx +40 -0
  204. package/templates/marketplace/app/error.tsx +26 -0
  205. package/templates/marketplace/app/globals.css +64 -0
  206. package/templates/marketplace/app/layout.tsx +60 -0
  207. package/templates/marketplace/app/listing/[id]/page.tsx +163 -0
  208. package/templates/marketplace/app/me/page.tsx +15 -0
  209. package/templates/marketplace/app/not-found.tsx +20 -0
  210. package/templates/marketplace/app/page.tsx +159 -0
  211. package/templates/marketplace/app/robots.ts +12 -0
  212. package/templates/marketplace/app/sell/page.tsx +26 -0
  213. package/templates/marketplace/app/sitemap.ts +14 -0
  214. package/templates/marketplace/app.ts +190 -0
  215. package/templates/marketplace/client/AuthNav.tsx +46 -0
  216. package/templates/marketplace/client/LiveTicker.tsx +104 -0
  217. package/templates/marketplace/client/LoginCard.tsx +130 -0
  218. package/templates/marketplace/client/MarketProvider.tsx +148 -0
  219. package/templates/marketplace/client/MyMarket.tsx +180 -0
  220. package/templates/marketplace/client/OfferPanel.tsx +355 -0
  221. package/templates/marketplace/client/SeedOnEmpty.tsx +26 -0
  222. package/templates/marketplace/client/SellForm.tsx +160 -0
  223. package/templates/marketplace/client/WatchButton.tsx +88 -0
  224. package/templates/marketplace/client/market.ts +341 -0
  225. package/templates/marketplace/functions/buyNow.ts +78 -0
  226. package/templates/marketplace/functions/makeOffer.ts +65 -0
  227. package/templates/marketplace/functions/respondToOffer.ts +62 -0
  228. package/templates/marketplace/functions/seedMarket.ts +90 -0
  229. package/templates/marketplace/gitignore +10 -0
  230. package/templates/marketplace/package.json +35 -0
  231. package/templates/marketplace/tsconfig.json +14 -0
  232. package/templates/marketplace/ui/badge.tsx +30 -0
  233. package/templates/marketplace/ui/button.tsx +49 -0
  234. package/templates/marketplace/ui/card.tsx +48 -0
  235. package/templates/marketplace/ui/input.tsx +17 -0
  236. package/templates/marketplace/ui/label.tsx +18 -0
  237. package/templates/marketplace/ui/textarea.tsx +17 -0
  238. package/templates/marketplace/ui/tokens.css +32 -0
  239. package/templates/marketplace/ui/utils.ts +6 -0
  240. package/templates/restaurant/.env.example +12 -0
  241. package/templates/restaurant/AGENTS.md +61 -0
  242. package/templates/restaurant/README.md +77 -0
  243. package/templates/restaurant/app/auth-form.tsx +129 -0
  244. package/templates/restaurant/app/dashboard/dashboard-client.tsx +263 -0
  245. package/templates/restaurant/app/dashboard/page.tsx +59 -0
  246. package/templates/restaurant/app/error.tsx +26 -0
  247. package/templates/restaurant/app/globals.css +148 -0
  248. package/templates/restaurant/app/layout.tsx +151 -0
  249. package/templates/restaurant/app/login/page.tsx +39 -0
  250. package/templates/restaurant/app/not-found.tsx +19 -0
  251. package/templates/restaurant/app/page.tsx +194 -0
  252. package/templates/restaurant/app/reservation-widget.tsx +359 -0
  253. package/templates/restaurant/app/robots.ts +12 -0
  254. package/templates/restaurant/app/sitemap.ts +9 -0
  255. package/templates/restaurant/app.ts +115 -0
  256. package/templates/restaurant/components/marketing.tsx +162 -0
  257. package/templates/restaurant/components/section-scroller.tsx +35 -0
  258. package/templates/restaurant/components/ui/button.tsx +56 -0
  259. package/templates/restaurant/components/ui/card.tsx +90 -0
  260. package/templates/restaurant/components.json +20 -0
  261. package/templates/restaurant/functions/cancelReservation.ts +26 -0
  262. package/templates/restaurant/functions/confirmReservation.ts +17 -0
  263. package/templates/restaurant/functions/createReservation.ts +92 -0
  264. package/templates/restaurant/functions/reservationsForOwner.ts +28 -0
  265. package/templates/restaurant/gitignore +10 -0
  266. package/templates/restaurant/lib/owner.ts +26 -0
  267. package/templates/restaurant/lib/reservation.ts +22 -0
  268. package/templates/restaurant/lib/site.config.ts +218 -0
  269. package/templates/restaurant/lib/slots.ts +55 -0
  270. package/templates/restaurant/lib/utils.ts +10 -0
  271. package/templates/restaurant/package.json +34 -0
  272. package/templates/restaurant/tsconfig.json +18 -0
  273. package/templates/shop/.env.example +32 -0
  274. package/templates/shop/AGENTS.md +61 -0
  275. package/templates/shop/README.md +102 -0
  276. package/templates/shop/app/auth-form.tsx +129 -0
  277. package/templates/shop/app/dashboard/dashboard-client.tsx +264 -0
  278. package/templates/shop/app/dashboard/page.tsx +59 -0
  279. package/templates/shop/app/error.tsx +26 -0
  280. package/templates/shop/app/globals.css +148 -0
  281. package/templates/shop/app/layout.tsx +160 -0
  282. package/templates/shop/app/login/page.tsx +39 -0
  283. package/templates/shop/app/not-found.tsx +19 -0
  284. package/templates/shop/app/page.tsx +95 -0
  285. package/templates/shop/app/robots.ts +12 -0
  286. package/templates/shop/app/shop-client.tsx +436 -0
  287. package/templates/shop/app/sitemap.ts +9 -0
  288. package/templates/shop/app/success/page.tsx +33 -0
  289. package/templates/shop/app.ts +134 -0
  290. package/templates/shop/components/marketing.tsx +96 -0
  291. package/templates/shop/components/section-scroller.tsx +35 -0
  292. package/templates/shop/components/ui/button.tsx +56 -0
  293. package/templates/shop/components/ui/card.tsx +90 -0
  294. package/templates/shop/components.json +20 -0
  295. package/templates/shop/functions/cancelOrder.ts +33 -0
  296. package/templates/shop/functions/checkout.ts +130 -0
  297. package/templates/shop/functions/fulfillOrder.ts +17 -0
  298. package/templates/shop/functions/markGroupPaid.ts +26 -0
  299. package/templates/shop/functions/ordersForOwner.ts +28 -0
  300. package/templates/shop/functions/releaseGroup.ts +36 -0
  301. package/templates/shop/functions/reserveCart.ts +87 -0
  302. package/templates/shop/functions/restockProduct.ts +23 -0
  303. package/templates/shop/functions/seedProducts.ts +30 -0
  304. package/templates/shop/functions/stripeWebhook.ts +72 -0
  305. package/templates/shop/gitignore +10 -0
  306. package/templates/shop/lib/owner.ts +26 -0
  307. package/templates/shop/lib/shop.ts +45 -0
  308. package/templates/shop/lib/site.config.ts +198 -0
  309. package/templates/shop/lib/utils.ts +10 -0
  310. package/templates/shop/package.json +35 -0
  311. package/templates/shop/tsconfig.json +18 -0
  312. package/templates/waitlist/.env.example +12 -0
  313. package/templates/waitlist/AGENTS.md +61 -0
  314. package/templates/waitlist/README.md +81 -0
  315. package/templates/waitlist/app/auth-form.tsx +129 -0
  316. package/templates/waitlist/app/dashboard/dashboard-client.tsx +297 -0
  317. package/templates/waitlist/app/dashboard/page.tsx +70 -0
  318. package/templates/waitlist/app/error.tsx +26 -0
  319. package/templates/waitlist/app/globals.css +148 -0
  320. package/templates/waitlist/app/layout.tsx +158 -0
  321. package/templates/waitlist/app/login/page.tsx +39 -0
  322. package/templates/waitlist/app/not-found.tsx +19 -0
  323. package/templates/waitlist/app/page.tsx +119 -0
  324. package/templates/waitlist/app/robots.ts +12 -0
  325. package/templates/waitlist/app/sitemap.ts +9 -0
  326. package/templates/waitlist/app/waitlist-hero.tsx +219 -0
  327. package/templates/waitlist/app.ts +134 -0
  328. package/templates/waitlist/components/marketing.tsx +96 -0
  329. package/templates/waitlist/components/ui/button.tsx +56 -0
  330. package/templates/waitlist/components/ui/card.tsx +90 -0
  331. package/templates/waitlist/components.json +20 -0
  332. package/templates/waitlist/functions/joinWaitlist.ts +82 -0
  333. package/templates/waitlist/functions/waitlistStats.ts +75 -0
  334. package/templates/waitlist/gitignore +10 -0
  335. package/templates/waitlist/lib/owner.ts +26 -0
  336. package/templates/waitlist/lib/site.config.ts +178 -0
  337. package/templates/waitlist/lib/stats.ts +30 -0
  338. package/templates/waitlist/lib/utils.ts +10 -0
  339. package/templates/waitlist/package.json +34 -0
  340. package/templates/waitlist/tsconfig.json +18 -0
@@ -0,0 +1,96 @@
1
+ import React from "react";
2
+
3
+ // Reusable presentational pieces for the landing page. All server-rendered —
4
+ // no client JS. Restyle here and the whole page follows. The brand accent
5
+ // (`text-brand`, `bg-brand-soft`) comes from CSS vars set on <html> in
6
+ // app/layout.tsx, which read lib/site.config.ts — so re-theming is one edit.
7
+
8
+ // Shared container: a contained, centered column.
9
+ export const WRAP = "mx-auto w-full max-w-3xl px-6";
10
+
11
+ export function Eyebrow({ children }: { children: React.ReactNode }) {
12
+ return (
13
+ <p className="font-mono text-[11px] font-semibold uppercase tracking-[0.14em] text-brand">
14
+ {children}
15
+ </p>
16
+ );
17
+ }
18
+
19
+ // "New / Coming soon"-style pill for the hero.
20
+ export function Badge({ children }: { children: React.ReactNode }) {
21
+ return (
22
+ <span className="inline-flex items-center gap-2 rounded-full border border-zinc-200 bg-white py-1 pl-1.5 pr-3 text-[13px] text-zinc-600 shadow-sm">
23
+ <span className="inline-block size-1.5 rounded-full bg-brand" />
24
+ {children}
25
+ </span>
26
+ );
27
+ }
28
+
29
+ export function Divider() {
30
+ return (
31
+ <div className={WRAP}>
32
+ <div className="border-t border-zinc-200/70" />
33
+ </div>
34
+ );
35
+ }
36
+
37
+ export function SectionHead({
38
+ eyebrow,
39
+ title,
40
+ body,
41
+ }: {
42
+ eyebrow: string;
43
+ title: string;
44
+ body?: string;
45
+ }) {
46
+ return (
47
+ <div>
48
+ <Eyebrow>{eyebrow}</Eyebrow>
49
+ <h2 className="mt-4 text-balance text-2xl font-semibold leading-[1.15] tracking-[-0.02em] sm:text-3xl">
50
+ {title}
51
+ </h2>
52
+ {body ? (
53
+ <p className="mt-4 max-w-xl text-[15px] leading-relaxed text-zinc-500">
54
+ {body}
55
+ </p>
56
+ ) : null}
57
+ </div>
58
+ );
59
+ }
60
+
61
+ // A grid of value props — icon + title + body.
62
+ export function FeatureGrid({
63
+ items,
64
+ }: {
65
+ items: { title: string; body: string; icon?: string }[];
66
+ }) {
67
+ return (
68
+ <div className="grid gap-6 sm:grid-cols-3">
69
+ {items.map((f) => (
70
+ <div key={f.title}>
71
+ {f.icon ? (
72
+ <span className="flex size-9 items-center justify-center rounded-lg bg-brand-soft text-brand">
73
+ {f.icon}
74
+ </span>
75
+ ) : null}
76
+ <h3 className="mt-4 text-[15px] font-semibold text-zinc-900">
77
+ {f.title}
78
+ </h3>
79
+ <p className="mt-2 text-[14px] leading-relaxed text-zinc-500">
80
+ {f.body}
81
+ </p>
82
+ </div>
83
+ ))}
84
+ </div>
85
+ );
86
+ }
87
+
88
+ // Initials for testimonial avatars, so the cards look finished without a photo.
89
+ export function initials(name: string) {
90
+ return name
91
+ .split(/\s+/)
92
+ .map((w) => w[0])
93
+ .join("")
94
+ .slice(0, 2)
95
+ .toUpperCase();
96
+ }
@@ -0,0 +1,35 @@
1
+ "use client";
2
+
3
+ import { useEffect } from "react";
4
+
5
+ // Makes in-page section links work. A hydrated Pylon page updates the URL for a
6
+ // plain `<a href="#section">` click but doesn't perform the browser's native
7
+ // fragment scroll, so the page jumps nowhere. This installs ONE delegated click
8
+ // handler that catches any same-page `#`/`/#` anchor and scrolls to it smoothly.
9
+ //
10
+ // Render it once (in the root layout). Renders nothing. Real route links should
11
+ // still use `<Link>` from @pylonsync/react — this only handles `#` anchors.
12
+ export function SectionScroller() {
13
+ useEffect(() => {
14
+ function onClick(e: MouseEvent) {
15
+ if (e.defaultPrevented || e.button !== 0 || e.metaKey || e.ctrlKey || e.shiftKey || e.altKey) {
16
+ return;
17
+ }
18
+ const target = e.target as Element | null;
19
+ const link = target?.closest?.('a[href^="#"], a[href^="/#"]') as HTMLAnchorElement | null;
20
+ if (!link) return;
21
+ const href = link.getAttribute("href") || "";
22
+ const id = href.slice(href.indexOf("#") + 1);
23
+ if (!id) return;
24
+ const el = document.getElementById(id);
25
+ if (!el) return; // target not on this page — leave it to the browser
26
+ e.preventDefault();
27
+ el.scrollIntoView({ block: "start" });
28
+ history.replaceState(null, "", "#" + id);
29
+ }
30
+ document.addEventListener("click", onClick);
31
+ return () => document.removeEventListener("click", onClick);
32
+ }, []);
33
+
34
+ return null;
35
+ }
@@ -0,0 +1,56 @@
1
+ import * as React from "react";
2
+ import { Slot } from "@radix-ui/react-slot";
3
+ import { cva, type VariantProps } from "class-variance-authority";
4
+
5
+ import { cn } from "@/lib/utils";
6
+
7
+ const buttonVariants = cva(
8
+ "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
9
+ {
10
+ variants: {
11
+ variant: {
12
+ default: "bg-primary text-primary-foreground hover:bg-primary/90",
13
+ destructive:
14
+ "bg-destructive text-white hover:bg-destructive/90",
15
+ outline:
16
+ "border border-input bg-background hover:bg-accent hover:text-accent-foreground",
17
+ secondary:
18
+ "bg-secondary text-secondary-foreground hover:bg-secondary/80",
19
+ ghost: "hover:bg-accent hover:text-accent-foreground",
20
+ link: "text-primary underline-offset-4 hover:underline",
21
+ },
22
+ size: {
23
+ default: "h-9 px-4 py-2",
24
+ sm: "h-8 rounded-md px-3 text-xs",
25
+ lg: "h-10 rounded-md px-8",
26
+ icon: "h-9 w-9",
27
+ },
28
+ },
29
+ defaultVariants: {
30
+ variant: "default",
31
+ size: "default",
32
+ },
33
+ },
34
+ );
35
+
36
+ export interface ButtonProps
37
+ extends React.ButtonHTMLAttributes<HTMLButtonElement>,
38
+ VariantProps<typeof buttonVariants> {
39
+ asChild?: boolean;
40
+ }
41
+
42
+ const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
43
+ ({ className, variant, size, asChild = false, ...props }, ref) => {
44
+ const Comp = asChild ? Slot : "button";
45
+ return (
46
+ <Comp
47
+ className={cn(buttonVariants({ variant, size, className }))}
48
+ ref={ref}
49
+ {...props}
50
+ />
51
+ );
52
+ },
53
+ );
54
+ Button.displayName = "Button";
55
+
56
+ export { Button, buttonVariants };
@@ -0,0 +1,90 @@
1
+ import * as React from "react";
2
+
3
+ import { cn } from "@/lib/utils";
4
+
5
+ function Card({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) {
6
+ return (
7
+ <div
8
+ data-slot="card"
9
+ className={cn(
10
+ "rounded-xl border bg-card text-card-foreground shadow-sm",
11
+ className,
12
+ )}
13
+ {...props}
14
+ />
15
+ );
16
+ }
17
+
18
+ function CardHeader({
19
+ className,
20
+ ...props
21
+ }: React.HTMLAttributes<HTMLDivElement>) {
22
+ return (
23
+ <div
24
+ data-slot="card-header"
25
+ className={cn("flex flex-col space-y-1.5 p-6", className)}
26
+ {...props}
27
+ />
28
+ );
29
+ }
30
+
31
+ function CardTitle({
32
+ className,
33
+ ...props
34
+ }: React.HTMLAttributes<HTMLDivElement>) {
35
+ return (
36
+ <div
37
+ data-slot="card-title"
38
+ className={cn("font-semibold leading-none tracking-tight", className)}
39
+ {...props}
40
+ />
41
+ );
42
+ }
43
+
44
+ function CardDescription({
45
+ className,
46
+ ...props
47
+ }: React.HTMLAttributes<HTMLDivElement>) {
48
+ return (
49
+ <div
50
+ data-slot="card-description"
51
+ className={cn("text-sm text-muted-foreground", className)}
52
+ {...props}
53
+ />
54
+ );
55
+ }
56
+
57
+ function CardContent({
58
+ className,
59
+ ...props
60
+ }: React.HTMLAttributes<HTMLDivElement>) {
61
+ return (
62
+ <div
63
+ data-slot="card-content"
64
+ className={cn("p-6 pt-0", className)}
65
+ {...props}
66
+ />
67
+ );
68
+ }
69
+
70
+ function CardFooter({
71
+ className,
72
+ ...props
73
+ }: React.HTMLAttributes<HTMLDivElement>) {
74
+ return (
75
+ <div
76
+ data-slot="card-footer"
77
+ className={cn("flex items-center p-6 pt-0", className)}
78
+ {...props}
79
+ />
80
+ );
81
+ }
82
+
83
+ export {
84
+ Card,
85
+ CardHeader,
86
+ CardFooter,
87
+ CardTitle,
88
+ CardDescription,
89
+ CardContent,
90
+ };
@@ -0,0 +1,20 @@
1
+ {
2
+ "$schema": "https://ui.shadcn.com/schema.json",
3
+ "style": "new-york",
4
+ "rsc": false,
5
+ "tsx": true,
6
+ "tailwind": {
7
+ "config": "",
8
+ "css": "app/globals.css",
9
+ "baseColor": "zinc",
10
+ "cssVariables": true
11
+ },
12
+ "aliases": {
13
+ "components": "@/components",
14
+ "utils": "@/lib/utils",
15
+ "ui": "@/components/ui",
16
+ "lib": "@/lib",
17
+ "hooks": "@/hooks"
18
+ },
19
+ "iconLibrary": "lucide"
20
+ }
@@ -0,0 +1,33 @@
1
+ import { mutation, v } from "@pylonsync/functions";
2
+ import { emailMatchesOwner } from "../lib/owner";
3
+
4
+ // cancelOrder — owner-only. Marks the order cancelled AND returns its units to
5
+ // stock: the Product.stock bump syncs to every open grid, so "N left" ticks
6
+ // back up (and a sold-out item can come back) live.
7
+ export default mutation<{ orderId: string }, { ok: boolean }>({
8
+ auth: "user",
9
+ args: { orderId: v.id("Order") },
10
+ async handler(ctx, args) {
11
+ const me = await ctx.db.get("User", ctx.auth.userId);
12
+ if (!emailMatchesOwner(me?.email as string | undefined, ctx.env.PYLON_OWNER_EMAIL)) {
13
+ throw ctx.error("POLICY_DENIED", "Only the owner can manage orders.");
14
+ }
15
+ const order = (await ctx.db.get("Order", args.orderId)) as
16
+ | { productSlug: string; qty: number; status: string }
17
+ | null;
18
+ if (!order) throw ctx.error("NOT_FOUND", "Order not found.");
19
+
20
+ await ctx.db.unsafe.update("Order", args.orderId, { status: "cancelled" });
21
+
22
+ // Restore the units (only if it wasn't already cancelled).
23
+ if (order.status !== "cancelled") {
24
+ const product = (await ctx.db.unsafe.lookup("Product", "slug", order.productSlug)) as
25
+ | { id: string; stock: number }
26
+ | null;
27
+ if (product) {
28
+ await ctx.db.unsafe.update("Product", product.id, { stock: product.stock + order.qty });
29
+ }
30
+ }
31
+ return { ok: true };
32
+ },
33
+ });
@@ -0,0 +1,130 @@
1
+ import { action, v } from "@pylonsync/functions";
2
+ import { stripeRequest, assertSafeRedirectUrl } from "@pylonsync/stripe";
3
+ import type { CheckoutResult } from "../lib/shop";
4
+
5
+ const EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
6
+ const CURRENCY = "usd";
7
+ // Stripe requires a Checkout Session to expire ≥30 min out; we hold the cart's
8
+ // stock until then, and the `checkout.session.expired` webhook returns it.
9
+ const SESSION_TTL_SECS = 35 * 60;
10
+
11
+ // checkout — turn a cart into a real order. An `action` (not a mutation) because
12
+ // it does network I/O (the Stripe API) on top of the DB write:
13
+ //
14
+ // 1. validate the cart + customer,
15
+ // 2. `runMutation("reserveCart")` to HOLD stock and record the order lines
16
+ // (race-safe, transactional — see reserveCart.ts), and
17
+ // 3. if STRIPE_SECRET_KEY is set, open a Stripe Checkout Session and hand the
18
+ // browser its URL; otherwise leave the order "reserved" for the owner.
19
+ //
20
+ // `auth: "public"` — a shopper has no account. PRIVACY: it returns only a redirect
21
+ // URL / status, never an Order row or anyone's email. The price is taken from the
22
+ // catalog inside reserveCart, never trusted from the client.
23
+ export default action<
24
+ {
25
+ items: { slug: string; qty: number }[];
26
+ customerName: string;
27
+ customerEmail: string;
28
+ successUrl: string;
29
+ cancelUrl: string;
30
+ },
31
+ CheckoutResult
32
+ >({
33
+ auth: "public",
34
+ args: {
35
+ items: v.array(v.object({ slug: v.string(), qty: v.int() })),
36
+ customerName: v.string(),
37
+ customerEmail: v.string(),
38
+ successUrl: v.string(),
39
+ cancelUrl: v.string(),
40
+ },
41
+ async handler(ctx, args): Promise<CheckoutResult> {
42
+ const name = args.customerName.trim();
43
+ const email = args.customerEmail.trim().toLowerCase();
44
+ if (name.length < 1 || name.length > 120) {
45
+ throw ctx.error("INVALID_ARGS", "Enter your name.");
46
+ }
47
+ if (!EMAIL_RE.test(email) || email.length > 254) {
48
+ throw ctx.error("INVALID_ARGS", "Enter a valid email address.");
49
+ }
50
+ const items = args.items.filter((i) => i && i.slug && Math.trunc(i.qty) >= 1);
51
+ if (items.length === 0) return { ok: false, reason: "empty", soldOut: [] };
52
+
53
+ const secretKey = ctx.env.STRIPE_SECRET_KEY?.trim();
54
+ const stripeOn = Boolean(secretKey);
55
+
56
+ // One id ties the cart's order lines together AND becomes the Stripe
57
+ // session's client_reference_id, so the webhook settles them as a unit.
58
+ const orderGroupId = crypto.randomUUID();
59
+
60
+ const held = await ctx.runMutation<{
61
+ ok: boolean;
62
+ lines: { slug: string; name: string; qty: number; unitPriceCents: number }[];
63
+ soldOut: string[];
64
+ }>("reserveCart", {
65
+ orderGroupId,
66
+ initialStatus: stripeOn ? "pending" : "reserved",
67
+ customerName: name,
68
+ customerEmail: email,
69
+ items,
70
+ });
71
+
72
+ if (!held.ok || held.lines.length === 0) {
73
+ return { ok: false, reason: "sold_out", soldOut: held.soldOut };
74
+ }
75
+
76
+ // No payment processor → the order is held as "reserved"; the owner follows
77
+ // up with a payment link. The store still works end-to-end with zero config.
78
+ if (!stripeOn) {
79
+ return { ok: true, mode: "reserved", orderGroupId, soldOut: held.soldOut };
80
+ }
81
+
82
+ // Stripe is configured → open a hosted Checkout Session. `price_data` prices
83
+ // each line inline, so there are no Stripe Price objects to pre-create.
84
+ // assertSafeRedirectUrl blocks an attacker-supplied success/cancel URL from
85
+ // pointing the post-payment redirect off to a malicious host: it allows the
86
+ // app's own host (PYLON_PUBLIC_URL / SITE_URL, plus localhost in dev).
87
+ const publicUrl = ctx.env.PYLON_PUBLIC_URL || ctx.env.SITE_URL;
88
+ const urlOpts = { extraOrigins: ctx.env.PYLON_CORS_ORIGIN };
89
+ assertSafeRedirectUrl(args.successUrl, publicUrl, urlOpts);
90
+ assertSafeRedirectUrl(args.cancelUrl, publicUrl, urlOpts);
91
+
92
+ // If the session can't be created (bad key, network error, Stripe down) we
93
+ // MUST release the held stock — there's no session that would ever expire to
94
+ // do it for us, so without this the units would be stranded off the shelf.
95
+ let session: { id: string; url: string } | undefined;
96
+ try {
97
+ session = await stripeRequest<{ id: string; url: string }>(
98
+ { secretKey: secretKey as string, idempotencyKey: orderGroupId },
99
+ "POST",
100
+ "/checkout/sessions",
101
+ {
102
+ mode: "payment",
103
+ success_url: args.successUrl,
104
+ cancel_url: args.cancelUrl,
105
+ client_reference_id: orderGroupId,
106
+ customer_email: email,
107
+ expires_at: Math.floor(Date.now() / 1000) + SESSION_TTL_SECS,
108
+ metadata: { orderGroupId },
109
+ line_items: held.lines.map((l) => ({
110
+ quantity: l.qty,
111
+ price_data: {
112
+ currency: CURRENCY,
113
+ unit_amount: l.unitPriceCents,
114
+ product_data: { name: l.name },
115
+ },
116
+ })),
117
+ },
118
+ );
119
+ } catch {
120
+ session = undefined;
121
+ }
122
+
123
+ if (!session?.url) {
124
+ await ctx.runMutation("releaseGroup", { orderGroupId });
125
+ throw ctx.error("STRIPE_ERROR", "Could not start checkout. Please try again.");
126
+ }
127
+
128
+ return { ok: true, mode: "stripe", checkoutUrl: session.url, soldOut: held.soldOut };
129
+ },
130
+ });
@@ -0,0 +1,17 @@
1
+ import { mutation, v } from "@pylonsync/functions";
2
+ import { emailMatchesOwner } from "../lib/owner";
3
+
4
+ // fulfillOrder — owner-only. Marks a reserved order fulfilled (shipped). Stock
5
+ // already came off the shelf at order time, so nothing else changes.
6
+ export default mutation<{ orderId: string }, { ok: boolean }>({
7
+ auth: "user",
8
+ args: { orderId: v.id("Order") },
9
+ async handler(ctx, args) {
10
+ const me = await ctx.db.get("User", ctx.auth.userId);
11
+ if (!emailMatchesOwner(me?.email as string | undefined, ctx.env.PYLON_OWNER_EMAIL)) {
12
+ throw ctx.error("POLICY_DENIED", "Only the owner can manage orders.");
13
+ }
14
+ await ctx.db.unsafe.update("Order", args.orderId, { status: "fulfilled" });
15
+ return { ok: true };
16
+ },
17
+ });
@@ -0,0 +1,26 @@
1
+ import { mutation, v } from "@pylonsync/functions";
2
+
3
+ // markGroupPaid — flip a cart's order lines from "pending" to "paid" once Stripe
4
+ // confirms the payment. Called only by the stripeWebhook action (internal), and
5
+ // idempotent: it only touches rows still "pending", so a re-delivered webhook
6
+ // (Stripe retries) is a no-op. Stock was already held at checkout, so nothing
7
+ // else changes here.
8
+ export default mutation<{ orderGroupId: string }, { ok: boolean; updated: number }>({
9
+ internal: true,
10
+ args: { orderGroupId: v.string() },
11
+ async handler(ctx, args) {
12
+ const rows = (await ctx.db.unsafe.list("Order")) as unknown as {
13
+ id: string;
14
+ orderGroupId: string;
15
+ status: string;
16
+ }[];
17
+ let updated = 0;
18
+ for (const o of rows) {
19
+ if (o.orderGroupId === args.orderGroupId && o.status === "pending") {
20
+ await ctx.db.unsafe.update("Order", o.id, { status: "paid" });
21
+ updated++;
22
+ }
23
+ }
24
+ return { ok: true, updated };
25
+ },
26
+ });
@@ -0,0 +1,28 @@
1
+ import { query } from "@pylonsync/functions";
2
+ import { emailMatchesOwner } from "../lib/owner";
3
+ import type { OrderRow, OwnerOrdersResult } from "../lib/shop";
4
+
5
+ // ordersForOwner — the owner's view of every order, INCLUDING the customer's
6
+ // name + email. The one function allowed to return that PII, gated to the
7
+ // configured owner (PYLON_OWNER_EMAIL via ctx.env).
8
+ //
9
+ // The dashboard calls it with `callFn` and re-fetches whenever the live, public
10
+ // Product set changes (stock moves on every order) — so new orders show up
11
+ // without a refresh, while contact details never travel over entity sync.
12
+ export default query({
13
+ auth: "user",
14
+ async handler(ctx): Promise<OwnerOrdersResult> {
15
+ const me = await ctx.db.get("User", ctx.auth.userId);
16
+ const email = (me?.email as string | undefined) ?? null;
17
+ if (!emailMatchesOwner(email, ctx.env.PYLON_OWNER_EMAIL)) {
18
+ return { authorized: false };
19
+ }
20
+
21
+ const rows = (await ctx.db.unsafe.list("Order")) as unknown as OrderRow[];
22
+ const orders = rows
23
+ .map((r) => ({ ...r }))
24
+ .sort((a, b) => (a.createdAt < b.createdAt ? 1 : a.createdAt > b.createdAt ? -1 : 0));
25
+
26
+ return { authorized: true, orders };
27
+ },
28
+ });
@@ -0,0 +1,36 @@
1
+ import { mutation, v } from "@pylonsync/functions";
2
+
3
+ // releaseGroup — return a cart's held stock to the shelf and mark its lines
4
+ // "cancelled". Called when a Stripe Checkout Session expires or its payment
5
+ // fails (via the webhook), or when checkout itself couldn't open the session.
6
+ // Internal + idempotent: it only acts on rows still "pending" (stock still
7
+ // held), so it never double-restores. The Product.stock bump syncs to every
8
+ // open grid, so "Sold out" can flip back to "N left" live.
9
+ export default mutation<{ orderGroupId: string }, { ok: boolean; restored: number }>({
10
+ internal: true,
11
+ args: { orderGroupId: v.string() },
12
+ async handler(ctx, args) {
13
+ const rows = (await ctx.db.unsafe.list("Order")) as unknown as {
14
+ id: string;
15
+ orderGroupId: string;
16
+ productSlug: string;
17
+ qty: number;
18
+ status: string;
19
+ }[];
20
+ let restored = 0;
21
+ for (const o of rows) {
22
+ if (o.orderGroupId !== args.orderGroupId || o.status !== "pending") continue;
23
+
24
+ await ctx.db.advisoryLock(`shop_product:${o.productSlug}`);
25
+ const product = (await ctx.db.unsafe.lookup("Product", "slug", o.productSlug)) as
26
+ | { id: string; stock: number }
27
+ | null;
28
+ if (product) {
29
+ await ctx.db.unsafe.update("Product", product.id, { stock: product.stock + o.qty });
30
+ }
31
+ await ctx.db.unsafe.update("Order", o.id, { status: "cancelled" });
32
+ restored += o.qty;
33
+ }
34
+ return { ok: true, restored };
35
+ },
36
+ });
@@ -0,0 +1,87 @@
1
+ import { mutation, v } from "@pylonsync/functions";
2
+
3
+ // reserveCart — hold stock for a whole cart and record its order lines, all in
4
+ // ONE transaction. Called only from the `checkout` action (never the public
5
+ // HTTP endpoint), so it's `internal: true`: it trusts its args because the
6
+ // action already validated them and decided the initial status.
7
+ //
8
+ // For each line it takes a per-product advisory lock, re-checks stock, and
9
+ // DECREMENTS Product.stock — so two carts can't both claim the last unit, and
10
+ // every open grid sees "N left" tick down live. A line whose product sold out
11
+ // in the meantime is skipped and reported in `soldOut` (partial carts still go
12
+ // through). The Order rows it inserts all share `orderGroupId` (= the Stripe
13
+ // Checkout Session's client_reference_id) so the webhook can settle them as one.
14
+ //
15
+ // `initialStatus` is set by the caller: "pending" when a Stripe payment is
16
+ // coming, "reserved" when there's no payment processor and the owner follows up.
17
+ export default mutation<
18
+ {
19
+ orderGroupId: string;
20
+ initialStatus: "pending" | "reserved";
21
+ customerName: string;
22
+ customerEmail: string;
23
+ items: { slug: string; qty: number }[];
24
+ },
25
+ {
26
+ ok: boolean;
27
+ lines: { slug: string; name: string; qty: number; unitPriceCents: number }[];
28
+ soldOut: string[];
29
+ }
30
+ >({
31
+ internal: true,
32
+ args: {
33
+ orderGroupId: v.string(),
34
+ initialStatus: v.string(),
35
+ customerName: v.string(),
36
+ customerEmail: v.string(),
37
+ items: v.array(
38
+ v.object({
39
+ slug: v.string(),
40
+ qty: v.int(),
41
+ }),
42
+ ),
43
+ },
44
+ async handler(ctx, args) {
45
+ const name = args.customerName.trim();
46
+ const email = args.customerEmail.trim().toLowerCase();
47
+ const lines: { slug: string; name: string; qty: number; unitPriceCents: number }[] = [];
48
+ const soldOut: string[] = [];
49
+
50
+ for (const item of args.items) {
51
+ const qty = Math.trunc(item.qty);
52
+ if (!Number.isFinite(qty) || qty < 1) continue;
53
+
54
+ // Serialize against other orders for this product so the re-check can't
55
+ // race; held until the tx commits.
56
+ await ctx.db.advisoryLock(`shop_product:${item.slug}`);
57
+
58
+ const product = (await ctx.db.unsafe.lookup("Product", "slug", item.slug)) as
59
+ | { id: string; name: string; priceCents: number; stock: number }
60
+ | null;
61
+ if (!product) continue;
62
+ if (product.stock <= 0) {
63
+ soldOut.push(product.name);
64
+ continue;
65
+ }
66
+
67
+ // Clamp to what's actually on the shelf; if some are left, take those.
68
+ const take = Math.min(qty, product.stock);
69
+ await ctx.db.unsafe.update("Product", product.id, { stock: product.stock - take });
70
+ await ctx.db.unsafe.insert("Order", {
71
+ orderGroupId: args.orderGroupId,
72
+ productSlug: item.slug,
73
+ productName: product.name,
74
+ qty: take,
75
+ unitPriceCents: product.priceCents,
76
+ customerName: name,
77
+ customerEmail: email,
78
+ status: args.initialStatus,
79
+ createdAt: new Date().toISOString(),
80
+ });
81
+ lines.push({ slug: item.slug, name: product.name, qty: take, unitPriceCents: product.priceCents });
82
+ if (take < qty) soldOut.push(product.name);
83
+ }
84
+
85
+ return { ok: lines.length > 0, lines, soldOut };
86
+ },
87
+ });