@pylonsync/create-pylon 0.3.273 → 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,104 @@
1
+ "use client";
2
+
3
+ import React from "react";
4
+ import { Link, db } from "@pylonsync/react";
5
+ import { Tag, BadgeCheck } from "lucide-react";
6
+ import { MarketProvider } from "./MarketProvider";
7
+ import { money, timeAgo, type Listing, type Offer } from "./market";
8
+
9
+ // Realtime activity strip. Two live queries — new listings AND completed sales
10
+ // (accepted offers) — merged into one feed. The moment anyone lists an item or
11
+ // a sale closes (in another tab, by another visitor), it slides in here. No
12
+ // polling, no refetch. This is the part SSR can't do; it's why the marketplace
13
+ // feels alive.
14
+ type Activity = {
15
+ key: string;
16
+ kind: "listed" | "sold";
17
+ title: string;
18
+ href: string;
19
+ amount?: number;
20
+ at: string;
21
+ };
22
+
23
+ function Ticker() {
24
+ const { data: listingData } = db.useQuery<Listing>("Listing", {
25
+ where: { status: "active" },
26
+ orderBy: { createdAt: "desc" },
27
+ limit: 8,
28
+ });
29
+ const { data: saleData } = db.useQuery<Offer>("Offer", {
30
+ where: { status: "accepted" },
31
+ orderBy: { createdAt: "desc" },
32
+ limit: 8,
33
+ });
34
+
35
+ const events: Activity[] = [
36
+ ...(listingData ?? []).map((l) => ({
37
+ key: `l-${l.id}`,
38
+ kind: "listed" as const,
39
+ title: l.title,
40
+ href: `/listing/${l.slug || l.id}`,
41
+ at: l.createdAt,
42
+ })),
43
+ ...(saleData ?? []).map((o) => ({
44
+ key: `s-${o.id}`,
45
+ kind: "sold" as const,
46
+ title: o.listingTitle,
47
+ href: `/listing/${o.listingId}`,
48
+ amount: o.amount,
49
+ at: o.createdAt,
50
+ })),
51
+ ]
52
+ .sort((a, b) => Date.parse(b.at) - Date.parse(a.at))
53
+ .slice(0, 10);
54
+
55
+ if (events.length === 0) return null;
56
+
57
+ return (
58
+ <div className="flex items-center gap-3 overflow-hidden rounded-lg border bg-card px-3 py-2 text-sm">
59
+ <span className="flex shrink-0 items-center gap-1.5 font-medium text-emerald-600">
60
+ <span className="relative flex size-2">
61
+ <span className="absolute inline-flex size-full animate-ping rounded-full bg-emerald-400 opacity-75" />
62
+ <span className="relative inline-flex size-2 rounded-full bg-emerald-500" />
63
+ </span>
64
+ Live
65
+ </span>
66
+ {/* Auto-scrolling marquee — no scrollbar. Items are doubled so the loop
67
+ (translateX -50%) is seamless; hover pauses so you can click. The
68
+ edge mask fades items in/out for a clean "live feed" look. */}
69
+ <div className="relative flex-1 overflow-hidden [mask-image:linear-gradient(to_right,transparent,#000_1.5rem,#000_calc(100%-1.5rem),transparent)]">
70
+ <div className="market-marquee flex w-max items-center gap-6">
71
+ {[...events, ...events].map((e, i) => (
72
+ <Link
73
+ key={`${e.key}-${i}`}
74
+ href={e.href}
75
+ className="flex shrink-0 items-center gap-1.5 whitespace-nowrap hover:underline"
76
+ aria-hidden={i >= events.length}
77
+ tabIndex={i >= events.length ? -1 : undefined}
78
+ >
79
+ {e.kind === "sold" ? (
80
+ <BadgeCheck className="size-3.5 text-emerald-600" />
81
+ ) : (
82
+ <Tag className="size-3.5 text-muted-foreground" />
83
+ )}
84
+ <span className="font-medium">{e.title}</span>
85
+ <span className="text-muted-foreground">
86
+ {e.kind === "sold" && e.amount != null
87
+ ? `sold · ${money(e.amount)}`
88
+ : timeAgo(e.at)}
89
+ </span>
90
+ </Link>
91
+ ))}
92
+ </div>
93
+ </div>
94
+ </div>
95
+ );
96
+ }
97
+
98
+ export function LiveTicker() {
99
+ return (
100
+ <MarketProvider fallback={null}>
101
+ <Ticker />
102
+ </MarketProvider>
103
+ );
104
+ }
@@ -0,0 +1,130 @@
1
+ "use client";
2
+
3
+ import React, { useState } from "react";
4
+ import { Button } from "../ui/button";
5
+ import { Input } from "../ui/input";
6
+ import { Label } from "../ui/label";
7
+ import { useAuth } from "./MarketProvider";
8
+ import { DEMO } from "./market";
9
+
10
+ // The sign-in surface shown wherever a write needs a real user (sell, make an
11
+ // offer, manage your market). Email/password, no verification email. Prefilled
12
+ // with the seeded demo account so it's one click to a working session.
13
+ export function LoginCard({
14
+ title = "Sign in to continue",
15
+ blurb = "Real email/password auth — no verification email. The demo account is prefilled; just hit Log in.",
16
+ }: {
17
+ title?: string;
18
+ blurb?: string;
19
+ }) {
20
+ const { signIn, signUp } = useAuth();
21
+ const [mode, setMode] = useState<"login" | "register">("login");
22
+ // Prefill the demo credentials so a reviewer can sign in instantly.
23
+ const [email, setEmail] = useState<string>(DEMO.email);
24
+ const [password, setPassword] = useState<string>(DEMO.password);
25
+ const [name, setName] = useState("");
26
+ const [busy, setBusy] = useState(false);
27
+ const [err, setErr] = useState<string | null>(null);
28
+
29
+ async function submit(e: React.FormEvent) {
30
+ e.preventDefault();
31
+ setBusy(true);
32
+ setErr(null);
33
+ try {
34
+ if (mode === "login") await signIn(email, password);
35
+ else await signUp(email, password, name);
36
+ } catch (e) {
37
+ setErr((e as Error).message ?? "Auth failed");
38
+ } finally {
39
+ setBusy(false);
40
+ }
41
+ }
42
+
43
+ return (
44
+ <div className="mx-auto max-w-sm space-y-4 rounded-xl border bg-card p-6">
45
+ <div>
46
+ <h2 className="text-lg font-semibold tracking-tight">{title}</h2>
47
+ <p className="mt-1 text-sm text-muted-foreground">{blurb}</p>
48
+ </div>
49
+ <form onSubmit={submit} className="space-y-3">
50
+ {mode === "register" ? (
51
+ <div className="space-y-1.5">
52
+ <Label htmlFor="lc-name">Name</Label>
53
+ <Input
54
+ id="lc-name"
55
+ value={name}
56
+ onChange={(e) => setName(e.target.value)}
57
+ placeholder="Pat Pylon"
58
+ />
59
+ </div>
60
+ ) : null}
61
+ <div className="space-y-1.5">
62
+ <Label htmlFor="lc-email">Email</Label>
63
+ <Input
64
+ id="lc-email"
65
+ type="email"
66
+ required
67
+ value={email}
68
+ onChange={(e) => setEmail(e.target.value)}
69
+ placeholder="you@example.com"
70
+ />
71
+ </div>
72
+ <div className="space-y-1.5">
73
+ <Label htmlFor="lc-password">Password</Label>
74
+ <Input
75
+ id="lc-password"
76
+ type="password"
77
+ required
78
+ minLength={8}
79
+ value={password}
80
+ onChange={(e) => setPassword(e.target.value)}
81
+ placeholder={mode === "register" ? "8+ characters" : ""}
82
+ />
83
+ </div>
84
+ {err ? <p className="text-sm text-destructive">{err}</p> : null}
85
+ <Button type="submit" disabled={busy} className="w-full">
86
+ {busy
87
+ ? "…"
88
+ : mode === "login"
89
+ ? "Log in"
90
+ : "Create account"}
91
+ </Button>
92
+ </form>
93
+ <p className="text-center text-xs text-muted-foreground">
94
+ {mode === "login" ? (
95
+ <>
96
+ No account?{" "}
97
+ <button
98
+ type="button"
99
+ className="font-medium text-foreground hover:underline"
100
+ onClick={() => {
101
+ setMode("register");
102
+ setEmail("");
103
+ setPassword("");
104
+ setErr(null);
105
+ }}
106
+ >
107
+ Sign up
108
+ </button>
109
+ </>
110
+ ) : (
111
+ <>
112
+ Have an account?{" "}
113
+ <button
114
+ type="button"
115
+ className="font-medium text-foreground hover:underline"
116
+ onClick={() => {
117
+ setMode("login");
118
+ setEmail(DEMO.email);
119
+ setPassword(DEMO.password);
120
+ setErr(null);
121
+ }}
122
+ >
123
+ Log in
124
+ </button>
125
+ </>
126
+ )}
127
+ </p>
128
+ </div>
129
+ );
130
+ }
@@ -0,0 +1,148 @@
1
+ "use client";
2
+
3
+ import React, {
4
+ createContext,
5
+ useCallback,
6
+ useContext,
7
+ useEffect,
8
+ useState,
9
+ } from "react";
10
+ import { db } from "@pylonsync/react";
11
+ import {
12
+ cacheDisplayName,
13
+ ensureDemoSeed,
14
+ ensureReadSession,
15
+ readIdentity,
16
+ signIn as doSignIn,
17
+ signOut as doSignOut,
18
+ signUp as doSignUp,
19
+ type Identity,
20
+ } from "./market";
21
+ import { LoginCard } from "./LoginCard";
22
+
23
+ // The sync engine (live queries, WebSocket) is browser-only, so every
24
+ // interactive island mounts behind this provider. It boots the client, keeps
25
+ // a guest session for READ connectivity (so the public ticker/grid run live
26
+ // for signed-out visitors), seeds the demo account, and exposes the current
27
+ // identity + auth actions. `identity` is null until the visitor signs in.
28
+
29
+ interface AuthContextValue {
30
+ /** The signed-in user, or null when browsing anonymously. */
31
+ identity: Identity | null;
32
+ /** True until the client has booted + read connectivity is established. */
33
+ ready: boolean;
34
+ signIn: (email: string, password: string) => Promise<void>;
35
+ signUp: (email: string, password: string, name: string) => Promise<void>;
36
+ signOut: () => Promise<void>;
37
+ }
38
+
39
+ const Ctx = createContext<AuthContextValue | null>(null);
40
+
41
+ /** The signed-in user, or null when anonymous. */
42
+ export function useIdentity(): Identity | null {
43
+ return useAuth().identity;
44
+ }
45
+
46
+ export function useAuth(): AuthContextValue {
47
+ const v = useContext(Ctx);
48
+ if (!v) throw new Error("useAuth must be used inside <MarketProvider>");
49
+ return v;
50
+ }
51
+
52
+ export function MarketProvider({
53
+ children,
54
+ fallback,
55
+ }: {
56
+ children: React.ReactNode;
57
+ fallback?: React.ReactNode;
58
+ }) {
59
+ const [ready, setReady] = useState(false);
60
+ const [identity, setIdentity] = useState<Identity | null>(null);
61
+
62
+ useEffect(() => {
63
+ let alive = true;
64
+ void (async () => {
65
+ await ensureReadSession();
66
+ // Fire-and-forget: makes the demo login work + seeds the catalog.
67
+ void ensureDemoSeed();
68
+ if (!alive) return;
69
+ setIdentity(readIdentity());
70
+ setReady(true);
71
+ })();
72
+
73
+ const onChange = () => setIdentity(readIdentity());
74
+ window.addEventListener("pylon-auth-changed", onChange);
75
+ window.addEventListener("storage", onChange);
76
+ return () => {
77
+ alive = false;
78
+ window.removeEventListener("pylon-auth-changed", onChange);
79
+ window.removeEventListener("storage", onChange);
80
+ };
81
+ }, []);
82
+
83
+ const signIn = useCallback(async (email: string, password: string) => {
84
+ await doSignIn(email, password);
85
+ setIdentity(readIdentity());
86
+ }, []);
87
+ const signUp = useCallback(
88
+ async (email: string, password: string, name: string) => {
89
+ await doSignUp(email, password, name);
90
+ setIdentity(readIdentity());
91
+ },
92
+ [],
93
+ );
94
+ const signOut = useCallback(async () => {
95
+ await doSignOut();
96
+ setIdentity(readIdentity());
97
+ }, []);
98
+
99
+ if (!ready) {
100
+ return (
101
+ <>
102
+ {fallback ?? (
103
+ <span className="text-xs text-muted-foreground">connecting…</span>
104
+ )}
105
+ </>
106
+ );
107
+ }
108
+
109
+ return (
110
+ <Ctx.Provider value={{ identity, ready, signIn, signUp, signOut }}>
111
+ {identity ? <DisplayNameSync userId={identity.userId} /> : null}
112
+ {children}
113
+ </Ctx.Provider>
114
+ );
115
+ }
116
+
117
+ // Keeps the cached displayName fresh from the live User row, so identity.name
118
+ // upgrades from the email handle to the real name once the row syncs in.
119
+ function DisplayNameSync({ userId }: { userId: string }) {
120
+ const { data } = db.useQueryOne<{ id: string; displayName?: string }>(
121
+ "User",
122
+ userId,
123
+ );
124
+ const name = data?.displayName;
125
+ useEffect(() => {
126
+ if (name) cacheDisplayName(name);
127
+ }, [name]);
128
+ return null;
129
+ }
130
+
131
+ /**
132
+ * Gate a write surface behind sign-in. Renders the children when the visitor
133
+ * is signed in; otherwise a prefilled demo login card. Reads (browse, ticker,
134
+ * listing detail) don't use this — they stay public.
135
+ */
136
+ export function AuthGate({
137
+ children,
138
+ title,
139
+ blurb,
140
+ }: {
141
+ children: React.ReactNode;
142
+ title?: string;
143
+ blurb?: string;
144
+ }) {
145
+ const { identity } = useAuth();
146
+ if (!identity) return <LoginCard title={title} blurb={blurb} />;
147
+ return <>{children}</>;
148
+ }
@@ -0,0 +1,180 @@
1
+ "use client";
2
+
3
+ import React from "react";
4
+ import { Link, db } from "@pylonsync/react";
5
+ import { Badge } from "../ui/badge";
6
+ import { Button } from "../ui/button";
7
+ import { AuthGate, MarketProvider, useIdentity } from "./MarketProvider";
8
+ import { Heart } from "lucide-react";
9
+ import { money, timeAgo, type Listing, type Offer, type Watch } from "./market";
10
+
11
+ type BadgeVariant = "default" | "secondary" | "destructive" | "outline" | "success" | "warning";
12
+
13
+ const statusVariant: Record<string, BadgeVariant> = {
14
+ pending: "warning",
15
+ accepted: "success",
16
+ declined: "outline",
17
+ };
18
+
19
+ function Dashboard() {
20
+ // Rendered inside <AuthGate>, so identity is non-null here.
21
+ const identity = useIdentity();
22
+ const userId = identity?.userId ?? "";
23
+ const name = identity?.name ?? "you";
24
+
25
+ // Three live queries, all scoped to me. Everything updates in place: a new
26
+ // offer on my listing, a seller answering my bid — no refresh.
27
+ const { data: listings } = db.useQuery<Listing>("Listing", {
28
+ where: { sellerId: userId },
29
+ orderBy: { createdAt: "desc" },
30
+ });
31
+ const { data: received } = db.useQuery<Offer>("Offer", {
32
+ where: { sellerId: userId },
33
+ orderBy: { createdAt: "desc" },
34
+ });
35
+ const { data: watching } = db.useQuery<Watch>("Watch", {
36
+ where: { userId },
37
+ orderBy: { createdAt: "desc" },
38
+ });
39
+ const { data: sent } = db.useQuery<Offer>("Offer", {
40
+ where: { buyerId: userId },
41
+ orderBy: { createdAt: "desc" },
42
+ });
43
+
44
+ const myListings = listings ?? [];
45
+ const myOffers = sent ?? [];
46
+ const watchlist = watching ?? [];
47
+ const inbound = received ?? [];
48
+ const pendingFor = (listingId: string) =>
49
+ inbound.filter((o) => o.listingId === listingId && o.status === "pending")
50
+ .length;
51
+
52
+ return (
53
+ <div className="space-y-10">
54
+ <header className="flex items-center justify-between">
55
+ <div>
56
+ <h1 className="text-2xl font-semibold tracking-tight">My Market</h1>
57
+ <p className="text-sm text-muted-foreground">
58
+ You're trading as <span className="font-medium">{name}</span>.
59
+ </p>
60
+ </div>
61
+ <Button asChild>
62
+ <Link href="/sell">Sell something</Link>
63
+ </Button>
64
+ </header>
65
+
66
+ <section className="space-y-3">
67
+ <h2 className="font-semibold">Your listings ({myListings.length})</h2>
68
+ {myListings.length === 0 ? (
69
+ <p className="rounded-lg border border-dashed p-6 text-center text-sm text-muted-foreground">
70
+ Nothing listed yet.{" "}
71
+ <Link href="/sell" className="underline">
72
+ Post your first item
73
+ </Link>
74
+ .
75
+ </p>
76
+ ) : (
77
+ <ul className="divide-y rounded-lg border">
78
+ {myListings.map((l) => (
79
+ <li key={l.id} className="flex items-center justify-between gap-3 p-3">
80
+ <Link href={`/listing/${l.slug || l.id}`} className="min-w-0 hover:underline">
81
+ <span className="truncate font-medium">{l.title}</span>
82
+ </Link>
83
+ <div className="flex shrink-0 items-center gap-2 text-sm">
84
+ <span className="font-semibold">{money(l.price)}</span>
85
+ {l.status === "sold" ? (
86
+ <Badge variant="success">Sold</Badge>
87
+ ) : pendingFor(l.id) > 0 ? (
88
+ <Badge variant="warning">
89
+ {pendingFor(l.id)} offer{pendingFor(l.id) > 1 ? "s" : ""}
90
+ </Badge>
91
+ ) : (
92
+ <Badge variant="outline">Active</Badge>
93
+ )}
94
+ </div>
95
+ </li>
96
+ ))}
97
+ </ul>
98
+ )}
99
+ </section>
100
+
101
+ <section className="space-y-3">
102
+ <h2 className="font-semibold">Offers you've made ({myOffers.length})</h2>
103
+ {myOffers.length === 0 ? (
104
+ <p className="rounded-lg border border-dashed p-6 text-center text-sm text-muted-foreground">
105
+ No offers out.{" "}
106
+ <Link href="/" className="underline">
107
+ Browse the market
108
+ </Link>
109
+ .
110
+ </p>
111
+ ) : (
112
+ <ul className="divide-y rounded-lg border">
113
+ {myOffers.map((o) => (
114
+ <li key={o.id} className="flex items-center justify-between gap-3 p-3">
115
+ <Link
116
+ href={`/listing/${o.listingId}`}
117
+ className="min-w-0 hover:underline"
118
+ >
119
+ <span className="truncate font-medium">{o.listingTitle}</span>
120
+ <span className="ml-2 text-sm text-muted-foreground">
121
+ {timeAgo(o.createdAt)}
122
+ </span>
123
+ </Link>
124
+ <div className="flex shrink-0 items-center gap-2 text-sm">
125
+ <span className="font-semibold tabular-nums">{money(o.amount)}</span>
126
+ <Badge variant={statusVariant[o.status] ?? "outline"}>
127
+ {o.status}
128
+ </Badge>
129
+ </div>
130
+ </li>
131
+ ))}
132
+ </ul>
133
+ )}
134
+ </section>
135
+
136
+ <section className="space-y-3">
137
+ <h2 className="flex items-center gap-2 font-semibold">
138
+ <Heart className="size-4 text-rose-500" />
139
+ Watching ({watchlist.length})
140
+ </h2>
141
+ {watchlist.length === 0 ? (
142
+ <p className="rounded-lg border border-dashed p-6 text-center text-sm text-muted-foreground">
143
+ Nothing saved yet. Tap the{" "}
144
+ <Heart className="inline size-3.5 align-text-bottom" /> on any
145
+ listing to watch it — your watchlist is private and syncs live.
146
+ </p>
147
+ ) : (
148
+ <ul className="divide-y rounded-lg border">
149
+ {watchlist.map((w) => (
150
+ <li key={w.id} className="flex items-center justify-between gap-3 p-3">
151
+ <Link
152
+ href={`/listing/${w.listingId}`}
153
+ className="min-w-0 truncate font-medium hover:underline"
154
+ >
155
+ {w.listingTitle}
156
+ </Link>
157
+ <span className="shrink-0 text-xs text-muted-foreground">
158
+ saved {timeAgo(w.createdAt)}
159
+ </span>
160
+ </li>
161
+ ))}
162
+ </ul>
163
+ )}
164
+ </section>
165
+ </div>
166
+ );
167
+ }
168
+
169
+ export function MyMarket() {
170
+ return (
171
+ <MarketProvider fallback={<p className="text-sm text-muted-foreground">Loading your market…</p>}>
172
+ <AuthGate
173
+ title="Sign in to see your market"
174
+ blurb="Your listings, offers received, and bids you've sent — all live. The demo account is prefilled; just hit Log in."
175
+ >
176
+ <Dashboard />
177
+ </AuthGate>
178
+ </MarketProvider>
179
+ );
180
+ }