@pylonsync/create-pylon 0.3.274 → 0.3.275

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 (323) 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 +286 -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 +207 -0
  17. package/templates/agency/app/robots.ts +12 -0
  18. package/templates/agency/app/sitemap.ts +9 -0
  19. package/templates/agency/app.ts +135 -0
  20. package/templates/agency/components/marketing.tsx +148 -0
  21. package/templates/agency/components/section-scroller.tsx +35 -0
  22. package/templates/agency/components/ui/button.tsx +56 -0
  23. package/templates/agency/components/ui/card.tsx +90 -0
  24. package/templates/agency/components.json +20 -0
  25. package/templates/agency/functions/bookInquiry.ts +42 -0
  26. package/templates/agency/functions/declineInquiry.ts +41 -0
  27. package/templates/agency/functions/inquiriesForOwner.ts +31 -0
  28. package/templates/agency/functions/seedCapacity.ts +26 -0
  29. package/templates/agency/functions/setCapacity.ts +32 -0
  30. package/templates/agency/functions/submitInquiry.ts +55 -0
  31. package/templates/agency/gitignore +10 -0
  32. package/templates/agency/lib/agency.ts +27 -0
  33. package/templates/agency/lib/owner.ts +26 -0
  34. package/templates/agency/lib/site.config.ts +239 -0
  35. package/templates/agency/lib/utils.ts +10 -0
  36. package/templates/agency/package.json +34 -0
  37. package/templates/agency/tsconfig.json +18 -0
  38. package/templates/ai-chat/.env.example +33 -0
  39. package/templates/ai-chat/AGENTS.md +61 -0
  40. package/templates/ai-chat/README.md +99 -0
  41. package/templates/ai-chat/app/auth-form.tsx +124 -0
  42. package/templates/ai-chat/app/chat-client.tsx +414 -0
  43. package/templates/ai-chat/app/error.tsx +26 -0
  44. package/templates/ai-chat/app/globals.css +148 -0
  45. package/templates/ai-chat/app/layout.tsx +75 -0
  46. package/templates/ai-chat/app/login/page.tsx +39 -0
  47. package/templates/ai-chat/app/not-found.tsx +19 -0
  48. package/templates/ai-chat/app/page.tsx +23 -0
  49. package/templates/ai-chat/app.ts +121 -0
  50. package/templates/ai-chat/components.json +20 -0
  51. package/templates/ai-chat/gitignore +10 -0
  52. package/templates/ai-chat/lib/site.config.ts +103 -0
  53. package/templates/ai-chat/lib/utils.ts +10 -0
  54. package/templates/ai-chat/package.json +34 -0
  55. package/templates/ai-chat/tsconfig.json +18 -0
  56. package/templates/ai-studio/.env.example +19 -0
  57. package/templates/ai-studio/AGENTS.md +61 -0
  58. package/templates/ai-studio/README.md +83 -0
  59. package/templates/ai-studio/app/auth-form.tsx +124 -0
  60. package/templates/ai-studio/app/error.tsx +26 -0
  61. package/templates/ai-studio/app/globals.css +148 -0
  62. package/templates/ai-studio/app/layout.tsx +75 -0
  63. package/templates/ai-studio/app/login/page.tsx +39 -0
  64. package/templates/ai-studio/app/not-found.tsx +19 -0
  65. package/templates/ai-studio/app/page.tsx +34 -0
  66. package/templates/ai-studio/app/studio-client.tsx +214 -0
  67. package/templates/ai-studio/app.ts +108 -0
  68. package/templates/ai-studio/components.json +20 -0
  69. package/templates/ai-studio/functions/_getGeneration.ts +25 -0
  70. package/templates/ai-studio/functions/_updateGeneration.ts +37 -0
  71. package/templates/ai-studio/functions/generate.ts +42 -0
  72. package/templates/ai-studio/functions/pollGeneration.ts +134 -0
  73. package/templates/ai-studio/gitignore +10 -0
  74. package/templates/ai-studio/lib/site.config.ts +80 -0
  75. package/templates/ai-studio/lib/studio.ts +52 -0
  76. package/templates/ai-studio/lib/utils.ts +10 -0
  77. package/templates/ai-studio/package.json +34 -0
  78. package/templates/ai-studio/tsconfig.json +18 -0
  79. package/templates/creator/.env.example +12 -0
  80. package/templates/creator/AGENTS.md +61 -0
  81. package/templates/creator/README.md +67 -0
  82. package/templates/creator/app/auth-form.tsx +129 -0
  83. package/templates/creator/app/dashboard/dashboard-client.tsx +297 -0
  84. package/templates/creator/app/dashboard/page.tsx +70 -0
  85. package/templates/creator/app/error.tsx +26 -0
  86. package/templates/creator/app/globals.css +148 -0
  87. package/templates/creator/app/layout.tsx +160 -0
  88. package/templates/creator/app/login/page.tsx +39 -0
  89. package/templates/creator/app/newsletter-signup.tsx +162 -0
  90. package/templates/creator/app/not-found.tsx +19 -0
  91. package/templates/creator/app/page.tsx +160 -0
  92. package/templates/creator/app/robots.ts +12 -0
  93. package/templates/creator/app/sitemap.ts +9 -0
  94. package/templates/creator/app.ts +134 -0
  95. package/templates/creator/components/marketing.tsx +148 -0
  96. package/templates/creator/components/section-scroller.tsx +35 -0
  97. package/templates/creator/components/ui/button.tsx +56 -0
  98. package/templates/creator/components/ui/card.tsx +90 -0
  99. package/templates/creator/components.json +20 -0
  100. package/templates/creator/functions/subscribe.ts +82 -0
  101. package/templates/creator/functions/subscriberStats.ts +75 -0
  102. package/templates/creator/gitignore +10 -0
  103. package/templates/creator/lib/owner.ts +26 -0
  104. package/templates/creator/lib/site.config.ts +173 -0
  105. package/templates/creator/lib/stats.ts +30 -0
  106. package/templates/creator/lib/utils.ts +10 -0
  107. package/templates/creator/package.json +34 -0
  108. package/templates/creator/tsconfig.json +18 -0
  109. package/templates/default/app/layout.tsx +26 -27
  110. package/templates/default/app/page.tsx +90 -274
  111. package/templates/default/lib/products.ts +9 -122
  112. package/templates/default/lib/site.config.ts +739 -0
  113. package/templates/default/lib/site.ts +14 -261
  114. package/templates/directory/.env.example +12 -0
  115. package/templates/directory/AGENTS.md +61 -0
  116. package/templates/directory/README.md +80 -0
  117. package/templates/directory/app/auth-form.tsx +129 -0
  118. package/templates/directory/app/dashboard/dashboard-client.tsx +205 -0
  119. package/templates/directory/app/dashboard/page.tsx +70 -0
  120. package/templates/directory/app/directory-browse.tsx +328 -0
  121. package/templates/directory/app/error.tsx +26 -0
  122. package/templates/directory/app/globals.css +148 -0
  123. package/templates/directory/app/layout.tsx +171 -0
  124. package/templates/directory/app/login/page.tsx +39 -0
  125. package/templates/directory/app/not-found.tsx +19 -0
  126. package/templates/directory/app/page.tsx +50 -0
  127. package/templates/directory/app/robots.ts +12 -0
  128. package/templates/directory/app/sitemap.ts +9 -0
  129. package/templates/directory/app/submit/page.tsx +30 -0
  130. package/templates/directory/app/submit-form.tsx +151 -0
  131. package/templates/directory/app.ts +146 -0
  132. package/templates/directory/components/marketing.tsx +148 -0
  133. package/templates/directory/components/section-scroller.tsx +35 -0
  134. package/templates/directory/components/ui/button.tsx +56 -0
  135. package/templates/directory/components/ui/card.tsx +90 -0
  136. package/templates/directory/components.json +20 -0
  137. package/templates/directory/functions/approveSubmission.ts +45 -0
  138. package/templates/directory/functions/rejectSubmission.ts +20 -0
  139. package/templates/directory/functions/seedListings.ts +33 -0
  140. package/templates/directory/functions/submissionsForOwner.ts +29 -0
  141. package/templates/directory/functions/submitListing.ts +63 -0
  142. package/templates/directory/functions/upvote.ts +24 -0
  143. package/templates/directory/gitignore +10 -0
  144. package/templates/directory/lib/directory.ts +45 -0
  145. package/templates/directory/lib/owner.ts +26 -0
  146. package/templates/directory/lib/site.config.ts +130 -0
  147. package/templates/directory/lib/utils.ts +10 -0
  148. package/templates/directory/package.json +34 -0
  149. package/templates/directory/tsconfig.json +18 -0
  150. package/templates/local-service/.env.example +12 -0
  151. package/templates/local-service/AGENTS.md +61 -0
  152. package/templates/local-service/README.md +82 -0
  153. package/templates/local-service/app/auth-form.tsx +129 -0
  154. package/templates/local-service/app/booking-widget.tsx +399 -0
  155. package/templates/local-service/app/dashboard/dashboard-client.tsx +304 -0
  156. package/templates/local-service/app/dashboard/page.tsx +63 -0
  157. package/templates/local-service/app/error.tsx +26 -0
  158. package/templates/local-service/app/globals.css +148 -0
  159. package/templates/local-service/app/layout.tsx +151 -0
  160. package/templates/local-service/app/login/page.tsx +39 -0
  161. package/templates/local-service/app/not-found.tsx +19 -0
  162. package/templates/local-service/app/page.tsx +233 -0
  163. package/templates/local-service/app/robots.ts +12 -0
  164. package/templates/local-service/app/sitemap.ts +9 -0
  165. package/templates/local-service/app.ts +131 -0
  166. package/templates/local-service/components/marketing.tsx +162 -0
  167. package/templates/local-service/components/section-scroller.tsx +35 -0
  168. package/templates/local-service/components/ui/button.tsx +56 -0
  169. package/templates/local-service/components/ui/card.tsx +90 -0
  170. package/templates/local-service/components.json +20 -0
  171. package/templates/local-service/functions/bookingsForOwner.ts +30 -0
  172. package/templates/local-service/functions/cancelBooking.ts +27 -0
  173. package/templates/local-service/functions/confirmBooking.ts +18 -0
  174. package/templates/local-service/functions/createBooking.ts +98 -0
  175. package/templates/local-service/gitignore +10 -0
  176. package/templates/local-service/lib/booking.ts +24 -0
  177. package/templates/local-service/lib/owner.ts +26 -0
  178. package/templates/local-service/lib/site.config.ts +232 -0
  179. package/templates/local-service/lib/slots.ts +97 -0
  180. package/templates/local-service/lib/utils.ts +10 -0
  181. package/templates/local-service/package.json +34 -0
  182. package/templates/local-service/tsconfig.json +18 -0
  183. package/templates/marketplace/.env.example +9 -0
  184. package/templates/marketplace/AGENTS.md +61 -0
  185. package/templates/marketplace/README.md +78 -0
  186. package/templates/marketplace/app/_components/CategoryIcon.tsx +40 -0
  187. package/templates/marketplace/app/error.tsx +26 -0
  188. package/templates/marketplace/app/globals.css +64 -0
  189. package/templates/marketplace/app/layout.tsx +60 -0
  190. package/templates/marketplace/app/listing/[id]/page.tsx +163 -0
  191. package/templates/marketplace/app/me/page.tsx +15 -0
  192. package/templates/marketplace/app/not-found.tsx +20 -0
  193. package/templates/marketplace/app/page.tsx +159 -0
  194. package/templates/marketplace/app/robots.ts +12 -0
  195. package/templates/marketplace/app/sell/page.tsx +26 -0
  196. package/templates/marketplace/app/sitemap.ts +14 -0
  197. package/templates/marketplace/app.ts +190 -0
  198. package/templates/marketplace/client/AuthNav.tsx +46 -0
  199. package/templates/marketplace/client/LiveTicker.tsx +104 -0
  200. package/templates/marketplace/client/LoginCard.tsx +130 -0
  201. package/templates/marketplace/client/MarketProvider.tsx +148 -0
  202. package/templates/marketplace/client/MyMarket.tsx +180 -0
  203. package/templates/marketplace/client/OfferPanel.tsx +355 -0
  204. package/templates/marketplace/client/SeedOnEmpty.tsx +26 -0
  205. package/templates/marketplace/client/SellForm.tsx +160 -0
  206. package/templates/marketplace/client/WatchButton.tsx +88 -0
  207. package/templates/marketplace/client/market.ts +341 -0
  208. package/templates/marketplace/functions/buyNow.ts +78 -0
  209. package/templates/marketplace/functions/makeOffer.ts +65 -0
  210. package/templates/marketplace/functions/respondToOffer.ts +62 -0
  211. package/templates/marketplace/functions/seedMarket.ts +90 -0
  212. package/templates/marketplace/gitignore +10 -0
  213. package/templates/marketplace/package.json +35 -0
  214. package/templates/marketplace/tsconfig.json +14 -0
  215. package/templates/marketplace/ui/badge.tsx +30 -0
  216. package/templates/marketplace/ui/button.tsx +49 -0
  217. package/templates/marketplace/ui/card.tsx +48 -0
  218. package/templates/marketplace/ui/input.tsx +17 -0
  219. package/templates/marketplace/ui/label.tsx +18 -0
  220. package/templates/marketplace/ui/textarea.tsx +17 -0
  221. package/templates/marketplace/ui/tokens.css +32 -0
  222. package/templates/marketplace/ui/utils.ts +6 -0
  223. package/templates/restaurant/.env.example +12 -0
  224. package/templates/restaurant/AGENTS.md +61 -0
  225. package/templates/restaurant/README.md +77 -0
  226. package/templates/restaurant/app/auth-form.tsx +129 -0
  227. package/templates/restaurant/app/dashboard/dashboard-client.tsx +263 -0
  228. package/templates/restaurant/app/dashboard/page.tsx +59 -0
  229. package/templates/restaurant/app/error.tsx +26 -0
  230. package/templates/restaurant/app/globals.css +148 -0
  231. package/templates/restaurant/app/layout.tsx +151 -0
  232. package/templates/restaurant/app/login/page.tsx +39 -0
  233. package/templates/restaurant/app/not-found.tsx +19 -0
  234. package/templates/restaurant/app/page.tsx +194 -0
  235. package/templates/restaurant/app/reservation-widget.tsx +359 -0
  236. package/templates/restaurant/app/robots.ts +12 -0
  237. package/templates/restaurant/app/sitemap.ts +9 -0
  238. package/templates/restaurant/app.ts +115 -0
  239. package/templates/restaurant/components/marketing.tsx +162 -0
  240. package/templates/restaurant/components/section-scroller.tsx +35 -0
  241. package/templates/restaurant/components/ui/button.tsx +56 -0
  242. package/templates/restaurant/components/ui/card.tsx +90 -0
  243. package/templates/restaurant/components.json +20 -0
  244. package/templates/restaurant/functions/cancelReservation.ts +26 -0
  245. package/templates/restaurant/functions/confirmReservation.ts +17 -0
  246. package/templates/restaurant/functions/createReservation.ts +92 -0
  247. package/templates/restaurant/functions/reservationsForOwner.ts +28 -0
  248. package/templates/restaurant/gitignore +10 -0
  249. package/templates/restaurant/lib/owner.ts +26 -0
  250. package/templates/restaurant/lib/reservation.ts +22 -0
  251. package/templates/restaurant/lib/site.config.ts +218 -0
  252. package/templates/restaurant/lib/slots.ts +55 -0
  253. package/templates/restaurant/lib/utils.ts +10 -0
  254. package/templates/restaurant/package.json +34 -0
  255. package/templates/restaurant/tsconfig.json +18 -0
  256. package/templates/shop/.env.example +32 -0
  257. package/templates/shop/AGENTS.md +61 -0
  258. package/templates/shop/README.md +102 -0
  259. package/templates/shop/app/auth-form.tsx +129 -0
  260. package/templates/shop/app/dashboard/dashboard-client.tsx +264 -0
  261. package/templates/shop/app/dashboard/page.tsx +59 -0
  262. package/templates/shop/app/error.tsx +26 -0
  263. package/templates/shop/app/globals.css +148 -0
  264. package/templates/shop/app/layout.tsx +160 -0
  265. package/templates/shop/app/login/page.tsx +39 -0
  266. package/templates/shop/app/not-found.tsx +19 -0
  267. package/templates/shop/app/page.tsx +95 -0
  268. package/templates/shop/app/robots.ts +12 -0
  269. package/templates/shop/app/shop-client.tsx +436 -0
  270. package/templates/shop/app/sitemap.ts +9 -0
  271. package/templates/shop/app/success/page.tsx +33 -0
  272. package/templates/shop/app.ts +134 -0
  273. package/templates/shop/components/marketing.tsx +96 -0
  274. package/templates/shop/components/section-scroller.tsx +35 -0
  275. package/templates/shop/components/ui/button.tsx +56 -0
  276. package/templates/shop/components/ui/card.tsx +90 -0
  277. package/templates/shop/components.json +20 -0
  278. package/templates/shop/functions/cancelOrder.ts +33 -0
  279. package/templates/shop/functions/checkout.ts +130 -0
  280. package/templates/shop/functions/fulfillOrder.ts +17 -0
  281. package/templates/shop/functions/markGroupPaid.ts +26 -0
  282. package/templates/shop/functions/ordersForOwner.ts +28 -0
  283. package/templates/shop/functions/releaseGroup.ts +36 -0
  284. package/templates/shop/functions/reserveCart.ts +87 -0
  285. package/templates/shop/functions/restockProduct.ts +23 -0
  286. package/templates/shop/functions/seedProducts.ts +30 -0
  287. package/templates/shop/functions/stripeWebhook.ts +72 -0
  288. package/templates/shop/gitignore +10 -0
  289. package/templates/shop/lib/owner.ts +26 -0
  290. package/templates/shop/lib/shop.ts +45 -0
  291. package/templates/shop/lib/site.config.ts +198 -0
  292. package/templates/shop/lib/utils.ts +10 -0
  293. package/templates/shop/package.json +35 -0
  294. package/templates/shop/tsconfig.json +18 -0
  295. package/templates/waitlist/.env.example +12 -0
  296. package/templates/waitlist/AGENTS.md +61 -0
  297. package/templates/waitlist/README.md +81 -0
  298. package/templates/waitlist/app/auth-form.tsx +129 -0
  299. package/templates/waitlist/app/dashboard/dashboard-client.tsx +297 -0
  300. package/templates/waitlist/app/dashboard/page.tsx +70 -0
  301. package/templates/waitlist/app/error.tsx +26 -0
  302. package/templates/waitlist/app/globals.css +148 -0
  303. package/templates/waitlist/app/layout.tsx +158 -0
  304. package/templates/waitlist/app/login/page.tsx +39 -0
  305. package/templates/waitlist/app/not-found.tsx +19 -0
  306. package/templates/waitlist/app/page.tsx +119 -0
  307. package/templates/waitlist/app/robots.ts +12 -0
  308. package/templates/waitlist/app/sitemap.ts +9 -0
  309. package/templates/waitlist/app/waitlist-hero.tsx +219 -0
  310. package/templates/waitlist/app.ts +134 -0
  311. package/templates/waitlist/components/marketing.tsx +96 -0
  312. package/templates/waitlist/components/ui/button.tsx +56 -0
  313. package/templates/waitlist/components/ui/card.tsx +90 -0
  314. package/templates/waitlist/components.json +20 -0
  315. package/templates/waitlist/functions/joinWaitlist.ts +82 -0
  316. package/templates/waitlist/functions/waitlistStats.ts +75 -0
  317. package/templates/waitlist/gitignore +10 -0
  318. package/templates/waitlist/lib/owner.ts +26 -0
  319. package/templates/waitlist/lib/site.config.ts +178 -0
  320. package/templates/waitlist/lib/stats.ts +30 -0
  321. package/templates/waitlist/lib/utils.ts +10 -0
  322. package/templates/waitlist/package.json +34 -0
  323. package/templates/waitlist/tsconfig.json +18 -0
@@ -0,0 +1,163 @@
1
+ import React, { Suspense, use } from "react";
2
+ import {
3
+ Link,
4
+ type GenerateMetadata,
5
+ type Metadata,
6
+ type PageProps,
7
+ type ServerData,
8
+ type SsrResponse,
9
+ } from "@pylonsync/react";
10
+ import { Badge } from "../../../ui/badge";
11
+ import { OfferPanel } from "../../../client/OfferPanel";
12
+ import { CategoryIcon } from "../../_components/CategoryIcon";
13
+ import { WatchButton } from "../../../client/WatchButton";
14
+ import {
15
+ gradient,
16
+ money,
17
+ conditionLabel,
18
+ type Listing,
19
+ } from "../../../client/market";
20
+
21
+ // Data-driven SEO: the title + description come from the listing itself,
22
+ // fetched on the server. `generateMetadata` is handed the same PageProps as
23
+ // the page (params + serverData), so it reads the row directly.
24
+ // Resolve a listing from the URL segment, which is its slug
25
+ // ("herman-miller-aeron-a1f3"). Falls back to a raw id lookup so older
26
+ // id-shaped links keep working.
27
+ async function resolveListing(
28
+ serverData: ServerData,
29
+ key: string,
30
+ ): Promise<Listing | null> {
31
+ return (
32
+ (await serverData.lookup<Listing>("Listing", "slug", key)) ??
33
+ (await serverData.get<Listing>("Listing", key))
34
+ );
35
+ }
36
+
37
+ export const generateMetadata: GenerateMetadata = async ({
38
+ params,
39
+ serverData,
40
+ }): Promise<Metadata> => {
41
+ const l = await resolveListing(serverData, params.id);
42
+ if (!l) return { title: "Listing not found · Pylon Market" };
43
+ return {
44
+ title: `${l.title} — ${money(l.price)} · Pylon Market`,
45
+ description:
46
+ l.description?.slice(0, 155) ||
47
+ `${l.title} for sale on Pylon Market (${conditionLabel(l.condition)}).`,
48
+ };
49
+ };
50
+
51
+ function Detail({
52
+ serverData,
53
+ response,
54
+ id,
55
+ }: {
56
+ serverData: ServerData;
57
+ response: SsrResponse;
58
+ id: string;
59
+ }) {
60
+ const listing = use(resolveListing(serverData, id));
61
+
62
+ if (!listing) {
63
+ response.setStatus(404);
64
+ return (
65
+ <div className="rounded-xl border border-dashed p-12 text-center">
66
+ <p className="font-medium">This listing is gone.</p>
67
+ <Link href="/" className="mt-2 inline-block text-sm underline">
68
+ Back to the market
69
+ </Link>
70
+ </div>
71
+ );
72
+ }
73
+
74
+ return (
75
+ <div className="space-y-6">
76
+ <Link
77
+ href="/"
78
+ className="text-sm text-muted-foreground hover:text-foreground"
79
+ >
80
+ ← Back to the market
81
+ </Link>
82
+
83
+ <div className="grid gap-8 md:grid-cols-2">
84
+ <div
85
+ className="relative flex aspect-square items-center justify-center rounded-2xl text-white/90"
86
+ style={{ background: gradient(listing.seed || listing.id) }}
87
+ >
88
+ <CategoryIcon category={listing.category} className="size-28" />
89
+ <WatchButton
90
+ listingId={listing.id}
91
+ listingTitle={listing.title}
92
+ className="absolute right-3 top-3"
93
+ />
94
+ {listing.status === "sold" ? (
95
+ <span className="absolute inset-0 grid place-items-center rounded-2xl bg-black/55 text-2xl font-bold uppercase tracking-wide">
96
+ Sold
97
+ </span>
98
+ ) : null}
99
+ </div>
100
+
101
+ <div className="flex flex-col gap-4">
102
+ <div className="space-y-1">
103
+ <div className="flex items-center gap-2 text-xs uppercase tracking-wider text-muted-foreground">
104
+ <span>{listing.category}</span>
105
+ <span>·</span>
106
+ <Badge variant="outline" className="text-[10px]">
107
+ {conditionLabel(listing.condition)}
108
+ </Badge>
109
+ </div>
110
+ <h1 className="text-2xl font-semibold tracking-tight">
111
+ {listing.title}
112
+ </h1>
113
+ <p className="text-3xl font-semibold tabular-nums">{money(listing.price)}</p>
114
+ <p className="text-sm text-muted-foreground">
115
+ Listed by <span className="font-medium">{listing.sellerName}</span>
116
+ </p>
117
+ </div>
118
+
119
+ {listing.description ? (
120
+ <p className="whitespace-pre-wrap text-sm leading-relaxed text-foreground/80">
121
+ {listing.description}
122
+ </p>
123
+ ) : null}
124
+
125
+ {/* Realtime client island: live offers, accept/decline. */}
126
+ <div className="mt-2">
127
+ <OfferPanel
128
+ listingId={listing.id}
129
+ sellerId={listing.sellerId}
130
+ sellerName={listing.sellerName}
131
+ title={listing.title}
132
+ price={listing.price}
133
+ status={listing.status}
134
+ />
135
+ </div>
136
+ </div>
137
+ </div>
138
+ </div>
139
+ );
140
+ }
141
+
142
+ export default function ListingPage({
143
+ params,
144
+ serverData,
145
+ response,
146
+ }: PageProps) {
147
+ return (
148
+ <Suspense
149
+ fallback={
150
+ <div className="grid gap-8 md:grid-cols-2">
151
+ <div className="aspect-square animate-pulse rounded-2xl bg-muted" />
152
+ <div className="space-y-3">
153
+ <div className="h-6 w-2/3 animate-pulse rounded bg-muted" />
154
+ <div className="h-8 w-1/3 animate-pulse rounded bg-muted" />
155
+ <div className="h-24 animate-pulse rounded bg-muted" />
156
+ </div>
157
+ </div>
158
+ }
159
+ >
160
+ <Detail serverData={serverData} response={response} id={params.id} />
161
+ </Suspense>
162
+ );
163
+ }
@@ -0,0 +1,15 @@
1
+ import React from "react";
2
+ import { type Metadata } from "@pylonsync/react";
3
+ import { MyMarket } from "../../client/MyMarket";
4
+
5
+ export const metadata: Metadata = {
6
+ title: "My Market · Pylon Market",
7
+ description: "Your listings and offers.",
8
+ robots: "noindex", // personal dashboard — keep it out of search
9
+ };
10
+
11
+ // Fully interactive dashboard (three live queries scoped to you), so the
12
+ // whole page is the client island.
13
+ export default function MePage() {
14
+ return <MyMarket />;
15
+ }
@@ -0,0 +1,20 @@
1
+ import React from "react";
2
+ import { Link, type NotFoundProps } from "@pylonsync/react";
3
+
4
+ // `app/not-found.tsx` → rendered at HTTP 404 for any unmatched URL (and when a
5
+ // page calls `response.notFound()` — e.g. a listing slug that doesn't resolve).
6
+ // Hydrated, so the link is a client nav.
7
+ export default function NotFound(_props: NotFoundProps) {
8
+ return (
9
+ <div className="mx-auto flex min-h-[60vh] max-w-3xl flex-col items-center justify-center px-6 text-center">
10
+ <h1 className="text-3xl font-semibold tracking-tight">404</h1>
11
+ <p className="mt-2 text-muted-foreground">We couldn&apos;t find that listing.</p>
12
+ <Link
13
+ href="/"
14
+ className="mt-6 inline-flex h-10 items-center rounded-md bg-foreground px-5 text-sm font-medium text-background transition hover:opacity-90"
15
+ >
16
+ Back to browse
17
+ </Link>
18
+ </div>
19
+ );
20
+ }
@@ -0,0 +1,159 @@
1
+ import React, { Suspense, use } from "react";
2
+ import {
3
+ Link,
4
+ type Metadata,
5
+ type PageProps,
6
+ type ServerData,
7
+ } from "@pylonsync/react";
8
+ import { Card } from "../ui/card";
9
+ import { Badge } from "../ui/badge";
10
+ import { LiveTicker } from "../client/LiveTicker";
11
+ import { SeedOnEmpty } from "../client/SeedOnEmpty";
12
+ import { CategoryIcon } from "./_components/CategoryIcon";
13
+ import { WatchButton } from "../client/WatchButton";
14
+ import { gradient, money, conditionLabel, type Listing } from "../client/market";
15
+
16
+ export const metadata: Metadata = {
17
+ title: "Pylon Market — buy & sell locally, live",
18
+ description:
19
+ "A live local marketplace. Server-rendered listings for SEO, realtime offers over the sync engine — one Pylon binary, one port.",
20
+ };
21
+
22
+ const CATEGORIES = [
23
+ "all", "furniture", "electronics", "cameras", "bikes", "audio", "kitchen",
24
+ "instruments", "outdoor", "apparel",
25
+ ];
26
+
27
+ // The grid suspends on the server-side read and streams in with real rows in
28
+ // the HTML (good for SEO + LCP). `serverData.query` runs through the same
29
+ // policy gate as a query function's ctx.db.
30
+ function Grid({
31
+ serverData,
32
+ category,
33
+ }: {
34
+ serverData: ServerData;
35
+ category: string;
36
+ }) {
37
+ const active = use(serverData.query<Listing>("Listing", { status: "active" }));
38
+ const sorted = [...active].sort((a, b) =>
39
+ b.createdAt.localeCompare(a.createdAt),
40
+ );
41
+ const listings =
42
+ category === "all"
43
+ ? sorted
44
+ : sorted.filter((l) => l.category === category);
45
+
46
+ if (listings.length === 0) {
47
+ return (
48
+ <>
49
+ <div className="rounded-xl border border-dashed p-12 text-center">
50
+ <p className="text-sm text-muted-foreground">
51
+ {active.length === 0
52
+ ? "Setting up a few sample listings…"
53
+ : "Nothing in this category yet."}
54
+ </p>
55
+ {category !== "all" ? (
56
+ <Link href="/" className="mt-2 inline-block text-sm underline">
57
+ See everything
58
+ </Link>
59
+ ) : null}
60
+ </div>
61
+ <SeedOnEmpty count={active.length} />
62
+ </>
63
+ );
64
+ }
65
+
66
+ return (
67
+ <div className="grid grid-cols-2 gap-4 sm:grid-cols-3 lg:grid-cols-4">
68
+ {listings.map((l) => (
69
+ <Link key={l.id} href={`/listing/${l.slug || l.id}`} className="group">
70
+ <Card className="flex flex-col overflow-hidden p-0 transition group-hover:-translate-y-0.5 group-hover:shadow-md">
71
+ <div
72
+ className="relative flex aspect-square items-center justify-center text-white/90"
73
+ style={{ background: gradient(l.seed || l.id) }}
74
+ >
75
+ <CategoryIcon category={l.category} className="size-14" />
76
+ <WatchButton
77
+ listingId={l.id}
78
+ listingTitle={l.title}
79
+ className="absolute right-2 top-2 size-8"
80
+ />
81
+ {l.status === "sold" ? (
82
+ <span className="absolute inset-0 grid place-items-center bg-black/55 text-sm font-bold uppercase tracking-wide">
83
+ Sold
84
+ </span>
85
+ ) : null}
86
+ </div>
87
+ <div className="flex flex-1 flex-col gap-1 p-3">
88
+ <span className="text-[10px] font-semibold uppercase tracking-wider text-muted-foreground">
89
+ {l.category}
90
+ </span>
91
+ <span className="line-clamp-2 min-h-[34px] text-sm font-medium leading-snug">
92
+ {l.title}
93
+ </span>
94
+ <div className="mt-1 flex items-center justify-between">
95
+ <span className="font-semibold tabular-nums">{money(l.price)}</span>
96
+ <Badge variant="outline" className="text-[10px]">
97
+ {conditionLabel(l.condition)}
98
+ </Badge>
99
+ </div>
100
+ </div>
101
+ </Card>
102
+ </Link>
103
+ ))}
104
+ </div>
105
+ );
106
+ }
107
+
108
+ export default function BrowsePage({ searchParams, serverData }: PageProps) {
109
+ const category =
110
+ typeof searchParams.category === "string" ? searchParams.category : "all";
111
+
112
+ return (
113
+ <div className="space-y-6">
114
+ <section className="space-y-1">
115
+ <h1 className="text-3xl font-semibold tracking-tight">
116
+ The local marketplace that's actually live
117
+ </h1>
118
+ <p className="text-muted-foreground">
119
+ Listings are server-rendered for search engines; offers are realtime.
120
+ Open this in two tabs — list in one, watch it appear in the other.
121
+ </p>
122
+ </section>
123
+
124
+ {/* Realtime client island */}
125
+ <LiveTicker />
126
+
127
+ <nav className="flex flex-wrap gap-2">
128
+ {CATEGORIES.map((c) => (
129
+ <Link
130
+ key={c}
131
+ href={c === "all" ? "/" : `/?category=${c}`}
132
+ className={`rounded-full border px-3 py-1 text-sm transition hover:bg-muted ${
133
+ category === c
134
+ ? "border-foreground bg-foreground text-background hover:bg-foreground"
135
+ : "text-muted-foreground"
136
+ }`}
137
+ >
138
+ {c}
139
+ </Link>
140
+ ))}
141
+ </nav>
142
+
143
+ <Suspense
144
+ fallback={
145
+ <div className="grid grid-cols-2 gap-4 sm:grid-cols-3 lg:grid-cols-4">
146
+ {Array.from({ length: 8 }).map((_, i) => (
147
+ <div
148
+ key={i}
149
+ className="aspect-[3/4] animate-pulse rounded-xl bg-muted"
150
+ />
151
+ ))}
152
+ </div>
153
+ }
154
+ >
155
+ <Grid serverData={serverData} category={category} />
156
+ </Suspense>
157
+ </div>
158
+ );
159
+ }
@@ -0,0 +1,12 @@
1
+ import type { Robots } from "@pylonsync/react";
2
+
3
+ // app/robots.ts → served at /robots.txt. Point SITE_URL at your domain in prod.
4
+ const SITE = process.env.SITE_URL ?? "http://localhost:4321";
5
+
6
+ export default function robots(): Robots {
7
+ return {
8
+ // Browse + listings are indexable; keep the personal inbox and API out.
9
+ rules: { userAgent: "*", allow: "/", disallow: ["/me", "/api/"] },
10
+ sitemap: `${SITE}/sitemap.xml`,
11
+ };
12
+ }
@@ -0,0 +1,26 @@
1
+ import React from "react";
2
+ import { type Metadata } from "@pylonsync/react";
3
+ import { SellForm } from "../../client/SellForm";
4
+
5
+ export const metadata: Metadata = {
6
+ title: "Sell an item · Pylon Market",
7
+ description: "List something for sale in seconds — buyers' offers arrive live.",
8
+ };
9
+
10
+ export default function SellPage() {
11
+ return (
12
+ <div className="mx-auto max-w-xl space-y-6">
13
+ <header className="space-y-1">
14
+ <h1 className="text-2xl font-semibold tracking-tight">List an item</h1>
15
+ <p className="text-sm text-muted-foreground">
16
+ It goes live instantly. Offers land in your{" "}
17
+ <a href="/me" className="underline">
18
+ My Market
19
+ </a>{" "}
20
+ inbox in realtime.
21
+ </p>
22
+ </header>
23
+ <SellForm />
24
+ </div>
25
+ );
26
+ }
@@ -0,0 +1,14 @@
1
+ import type { Sitemap } from "@pylonsync/react";
2
+
3
+ // app/sitemap.ts → served at /sitemap.xml. Point SITE_URL at your domain in
4
+ // production. We list the static surfaces here; for a real catalog, map over
5
+ // active listings (read them via the entity API) and add a `/listing/<slug>`
6
+ // entry per row.
7
+ const SITE = process.env.SITE_URL ?? "http://localhost:4321";
8
+
9
+ export default async function sitemap(): Promise<Sitemap> {
10
+ return [
11
+ { url: `${SITE}/`, changeFrequency: "hourly", priority: 1 },
12
+ { url: `${SITE}/sell`, changeFrequency: "monthly", priority: 0.5 },
13
+ ];
14
+ }
@@ -0,0 +1,190 @@
1
+ /**
2
+ * Pylon Market — a live local marketplace.
3
+ *
4
+ * Anyone (a guest session) can list an item for sale; anyone else can make
5
+ * an offer. Sellers watch offers arrive in realtime and accept/decline them.
6
+ *
7
+ * The Pylon story this demo tells:
8
+ * - The browse grid (`/`) and each listing page (`/listing/:id`) are
9
+ * SERVER-RENDERED with real rows from the database (good for SEO + LCP) —
10
+ * view source and the products are in the HTML, not fetched later.
11
+ * - The interactive, realtime bits — the "just listed" ticker, the live
12
+ * offers on a listing, your inbox on `/me` — ride the sync engine: a
13
+ * single `useQuery` fans every write out to every open tab instantly.
14
+ * - One binary, one port. SSR + REST + WebSockets all from `pylon dev`.
15
+ * No Next.js app, no separate realtime service.
16
+ */
17
+ import {
18
+ entity,
19
+ field,
20
+ policy,
21
+ buildManifest,
22
+ discoverAppRoutes,
23
+ } from "@pylonsync/sdk";
24
+
25
+ // Accounts. Email/password auth is built in: registering through
26
+ // /api/auth/password/register hashes the password and writes this row.
27
+ // `passwordHash` is server-only — never serialized to clients.
28
+ const User = entity(
29
+ "User",
30
+ {
31
+ email: field.string(),
32
+ displayName: field.string(),
33
+ // Set by the auth subsystem on register (a default avatar tint).
34
+ avatarColor: field.string().optional(),
35
+ passwordHash: field.string().serverOnly().optional(),
36
+ createdAt: field.datetime().defaultNow(),
37
+ },
38
+ {
39
+ indexes: [{ name: "by_email", fields: ["email"], unique: true }],
40
+ },
41
+ );
42
+
43
+ // A thing for sale. `seed` drives a deterministic gradient "photo" so the
44
+ // demo needs no image hosting. `status` flips active → sold when an offer
45
+ // is accepted.
46
+ //
47
+ // `sellerId: field.owner()` is what lets SellForm create a listing with a
48
+ // plain, optimistic `db.insert` (it shows in the live ticker the instant
49
+ // you post — no server round-trip) while the seller id stays unspoofable:
50
+ // the framework stamps it from the session and rejects any forged value.
51
+ // No createListing function needed. `status` + `createdAt` default
52
+ // server-side so the client doesn't have to send them.
53
+ const Listing = entity(
54
+ "Listing",
55
+ {
56
+ sellerId: field.string().owner(),
57
+ sellerName: field.string(),
58
+ title: field.string(),
59
+ // Human-readable URL key: "herman-miller-aeron-size-b-a1f3". Unique so it
60
+ // addresses exactly one listing; the detail route resolves by it.
61
+ slug: field.string().unique(),
62
+ description: field.string(),
63
+ price: field.float(),
64
+ category: field.string(),
65
+ condition: field.string(), // new | like-new | good | fair
66
+ status: field.string().default("active"), // active | sold
67
+ seed: field.string(),
68
+ createdAt: field.datetime().defaultNow(),
69
+ },
70
+ {
71
+ indexes: [
72
+ { name: "by_status", fields: ["status"], unique: false },
73
+ { name: "by_seller", fields: ["sellerId"], unique: false },
74
+ { name: "by_created", fields: ["createdAt"], unique: false },
75
+ { name: "by_slug", fields: ["slug"], unique: true },
76
+ ],
77
+ },
78
+ );
79
+
80
+ // A buyer's bid on a listing. The seller responds; accepting marks the
81
+ // listing sold and auto-declines the rest.
82
+ const Offer = entity(
83
+ "Offer",
84
+ {
85
+ listingId: field.string(),
86
+ listingTitle: field.string(),
87
+ sellerId: field.string(),
88
+ // `buyerId: field.owner()` keeps the bidder unspoofable on the
89
+ // optimistic db.useMutation path too — even though makeOffer also
90
+ // stamps it server-side, the field-level guarantee is defense in
91
+ // depth (and documents intent).
92
+ buyerId: field.string().owner(),
93
+ buyerName: field.string(),
94
+ amount: field.float(),
95
+ message: field.string().optional(),
96
+ status: field.string().default("pending"), // pending | accepted | declined
97
+ createdAt: field.datetime().defaultNow(),
98
+ },
99
+ {
100
+ indexes: [
101
+ { name: "by_listing", fields: ["listingId"], unique: false },
102
+ { name: "by_buyer", fields: ["buyerId"], unique: false },
103
+ { name: "by_seller", fields: ["sellerId"], unique: false },
104
+ ],
105
+ },
106
+ );
107
+
108
+ // A buyer's saved listing. Private to the watcher (owner-scoped read), so
109
+ // your watchlist is yours alone. `listingTitle` is denormalized so the
110
+ // "Watching" list renders without a join.
111
+ const Watch = entity(
112
+ "Watch",
113
+ {
114
+ userId: field.string().owner(),
115
+ listingId: field.string(),
116
+ listingTitle: field.string(),
117
+ createdAt: field.datetime().defaultNow(),
118
+ },
119
+ {
120
+ indexes: [
121
+ { name: "by_user", fields: ["userId"], unique: false },
122
+ // One watch per (user, listing) — toggling the heart inserts/deletes
123
+ // this row.
124
+ { name: "by_user_listing", fields: ["userId", "listingId"], unique: true },
125
+ ],
126
+ },
127
+ );
128
+
129
+ // Public marketplace: everyone can read listings + offers (so buyers and
130
+ // sellers both see the live state). Writes require a session and are
131
+ // owner-scoped; the heavy lifting (accept = mark sold + decline siblings)
132
+ // runs in functions where it can enforce "only the seller responds".
133
+ // Signed-in users can read profiles (to render seller/buyer names). The
134
+ // auth subsystem owns writes — registration/login go through
135
+ // /api/auth/password/*, not the entity API, so direct inserts/updates are
136
+ // closed off.
137
+ const userPolicy = policy({
138
+ name: "user_access",
139
+ entity: "User",
140
+ allowRead: "auth.userId != null",
141
+ allowInsert: "false",
142
+ allowUpdate: "false",
143
+ allowDelete: "false",
144
+ });
145
+
146
+ // Watchlists are private: you can only read, add to, or remove from your own.
147
+ const watchPolicy = policy({
148
+ name: "watch_access",
149
+ entity: "Watch",
150
+ allowRead: "auth.userId == data.userId",
151
+ allowInsert: "auth.userId != null",
152
+ allowUpdate: "false",
153
+ allowDelete: "auth.userId == data.userId",
154
+ });
155
+
156
+ const listingPolicy = policy({
157
+ name: "listing_access",
158
+ entity: "Listing",
159
+ allowRead: "true",
160
+ allowInsert: "auth.userId != null",
161
+ allowUpdate: "auth.userId == data.sellerId",
162
+ allowDelete: "auth.userId == data.sellerId",
163
+ });
164
+
165
+ const offerPolicy = policy({
166
+ name: "offer_access",
167
+ entity: "Offer",
168
+ allowRead: "true",
169
+ allowInsert: "auth.userId != null",
170
+ // Buyers can withdraw their own offer; the seller's accept/decline goes
171
+ // through respondToOffer (which checks ownership of the listing).
172
+ allowUpdate: "auth.userId == data.buyerId || auth.userId == data.sellerId",
173
+ allowDelete: "auth.userId == data.buyerId",
174
+ });
175
+
176
+ const manifest = buildManifest({
177
+ name: "__APP_NAME__",
178
+ version: "0.1.0",
179
+ entities: [User, Listing, Offer, Watch],
180
+ queries: [],
181
+ actions: [],
182
+ policies: [userPolicy, listingPolicy, offerPolicy, watchPolicy],
183
+ // File-based SSR routing: app/**/page.tsx. One binary serves the frontend
184
+ // and the API on one port.
185
+ routes: await discoverAppRoutes(),
186
+ });
187
+
188
+ console.log(JSON.stringify(manifest, null, 2));
189
+
190
+ export default manifest;
@@ -0,0 +1,46 @@
1
+ "use client";
2
+
3
+ import React from "react";
4
+ import { Link } from "@pylonsync/react";
5
+ import { Button } from "../ui/button";
6
+ import { MarketProvider, useAuth } from "./MarketProvider";
7
+
8
+ // Compact sign-in state for the header. Signed out → a link to /sell (which
9
+ // gates with the prefilled demo login). Signed in → your name + a sign-out
10
+ // button. Lives behind MarketProvider like every other island.
11
+ function Nav() {
12
+ const { identity, signOut } = useAuth();
13
+ if (!identity) {
14
+ return (
15
+ <Link
16
+ href="/sell"
17
+ className="rounded-md border px-3 py-1.5 text-sm font-medium transition hover:bg-muted"
18
+ >
19
+ Sign in
20
+ </Link>
21
+ );
22
+ }
23
+ return (
24
+ <div className="flex items-center gap-2 text-sm">
25
+ <span className="hidden text-muted-foreground sm:inline">
26
+ {identity.name}
27
+ </span>
28
+ <Button
29
+ variant="ghost"
30
+ size="sm"
31
+ className="text-muted-foreground"
32
+ onClick={() => void signOut()}
33
+ >
34
+ Sign out
35
+ </Button>
36
+ </div>
37
+ );
38
+ }
39
+
40
+ export function AuthNav() {
41
+ return (
42
+ <MarketProvider fallback={<span className="w-16" />}>
43
+ <Nav />
44
+ </MarketProvider>
45
+ );
46
+ }