@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,129 @@
1
+ "use client";
2
+
3
+ import React, { useState } from "react";
4
+ import {
5
+ passwordLogin,
6
+ passwordRegister,
7
+ persistSession,
8
+ ApiError,
9
+ } from "@pylonsync/client";
10
+
11
+ // The owner's email/password form — one form, two modes. It calls the built-in
12
+ // auth API directly (`passwordLogin` / `passwordRegister` POST to
13
+ // `/api/auth/password/*`), then `persistSession` writes the freshly-minted
14
+ // token to local storage so the sync engine + `callFn` authenticate AS THE
15
+ // OWNER on the next load. This step matters here specifically: the landing page
16
+ // mints an anonymous guest session (for the live slot counter), and without
17
+ // persisting the real session that stale guest token would shadow the owner's
18
+ // — so the owner-only `inquiriesForOwner` call would come back as a guest and
19
+ // get rejected. We then do a full navigation to /dashboard so the SSR runtime
20
+ // re-resolves auth from the HttpOnly cookie and renders server-side.
21
+ //
22
+ // A studio is single-tenant: there's no public sign-up, just the owner creating
23
+ // their one account. Whoever signs in only sees data if their email matches
24
+ // PYLON_OWNER_EMAIL — enforced by the inquiriesForOwner function.
25
+ export function AuthForm() {
26
+ const [mode, setMode] = useState<"login" | "register">("login");
27
+ const [email, setEmail] = useState("");
28
+ const [password, setPassword] = useState("");
29
+ const [error, setError] = useState<string | null>(null);
30
+ const [pending, setPending] = useState(false);
31
+
32
+ async function onSubmit(e: React.FormEvent) {
33
+ e.preventDefault();
34
+ setError(null);
35
+ setPending(true);
36
+ try {
37
+ const session =
38
+ mode === "login"
39
+ ? await passwordLogin({ email, password })
40
+ : await passwordRegister({ email, password });
41
+ // Make this session authoritative, replacing any anonymous guest token.
42
+ persistSession(session);
43
+ window.location.assign("/dashboard");
44
+ } catch (err) {
45
+ setError(messageFor(err));
46
+ setPending(false);
47
+ }
48
+ }
49
+
50
+ return (
51
+ <div className="space-y-5">
52
+ <form onSubmit={onSubmit} className="space-y-4">
53
+ <label className="block">
54
+ <span className="mb-1.5 block text-[13px] font-medium text-zinc-700">Email</span>
55
+ <input
56
+ type="email"
57
+ value={email}
58
+ onChange={(e) => setEmail(e.target.value)}
59
+ required
60
+ autoComplete="email"
61
+ placeholder="you@yourbusiness.com"
62
+ className="h-10 w-full rounded-lg border border-zinc-300 bg-white px-3 text-sm text-zinc-900 outline-none transition placeholder:text-zinc-400 focus:border-brand focus:ring-2 focus:ring-brand/20"
63
+ />
64
+ </label>
65
+ <label className="block">
66
+ <span className="mb-1.5 block text-[13px] font-medium text-zinc-700">Password</span>
67
+ <input
68
+ type="password"
69
+ value={password}
70
+ onChange={(e) => setPassword(e.target.value)}
71
+ required
72
+ autoComplete={mode === "login" ? "current-password" : "new-password"}
73
+ placeholder={mode === "login" ? "Your password" : "At least 10 characters"}
74
+ className="h-10 w-full rounded-lg border border-zinc-300 bg-white px-3 text-sm text-zinc-900 outline-none transition placeholder:text-zinc-400 focus:border-brand focus:ring-2 focus:ring-brand/20"
75
+ />
76
+ </label>
77
+ {error ? (
78
+ <p className="rounded-lg border border-red-200 bg-red-50 px-3 py-2 text-[13px] leading-snug text-red-700">
79
+ {error}
80
+ </p>
81
+ ) : null}
82
+ <button
83
+ type="submit"
84
+ disabled={pending}
85
+ className="inline-flex h-10 w-full items-center justify-center rounded-lg bg-zinc-900 text-sm font-medium text-white transition-colors hover:bg-zinc-700 disabled:opacity-60"
86
+ >
87
+ {pending ? "…" : mode === "login" ? "Sign in" : "Create account"}
88
+ </button>
89
+ </form>
90
+
91
+ <p className="text-center text-[13px] text-zinc-500">
92
+ {mode === "login" ? "First time here?" : "Already have an account?"}{" "}
93
+ <button
94
+ type="button"
95
+ onClick={() => {
96
+ setMode(mode === "login" ? "register" : "login");
97
+ setError(null);
98
+ }}
99
+ className="font-medium text-zinc-900 underline underline-offset-2"
100
+ >
101
+ {mode === "login" ? "Create the owner account" : "Sign in"}
102
+ </button>
103
+ </p>
104
+ </div>
105
+ );
106
+ }
107
+
108
+ // Map the framework's auth error codes to friendly copy. `ApiError` carries a
109
+ // stable `.code` so you branch on the code, not the message.
110
+ function messageFor(err: unknown): string {
111
+ if (err instanceof ApiError) {
112
+ switch (err.code) {
113
+ case "INVALID_CREDENTIALS":
114
+ return "Wrong email or password.";
115
+ case "USER_EXISTS":
116
+ return "That email is already registered — sign in instead.";
117
+ case "WEAK_PASSWORD":
118
+ return "Pick a longer password — at least 10 characters.";
119
+ case "PWNED_PASSWORD":
120
+ return "That password has appeared in a known data breach. Choose a different one.";
121
+ case "RATE_LIMITED":
122
+ return "Too many attempts — try again in a minute.";
123
+ default:
124
+ return err.message;
125
+ }
126
+ }
127
+ if (err instanceof Error) return err.message;
128
+ return "Something went wrong. Try again.";
129
+ }
@@ -0,0 +1,258 @@
1
+ "use client";
2
+
3
+ import React, { useEffect, useState } from "react";
4
+ import { db, callFn } from "@pylonsync/react";
5
+ import { EnsureGuest } from "@pylonsync/client";
6
+ import { siteConfig } from "@/lib/site.config";
7
+
8
+ // The realtime pieces of the page, both driven by the public, PII-free Capacity
9
+ // row via `db.useQuery("Capacity")` — so when the owner books a project from the
10
+ // dashboard, the "N slots open" number drops live in every open tab. No refresh.
11
+ //
12
+ // • <LiveSlots> — the hero pill ("3 project slots open · Q3 2026").
13
+ // • <ContactForm> — the "start a project" form. submitInquiry is a public
14
+ // mutation, so it works for anonymous visitors; the Inquiry
15
+ // it writes is pure PII and can never be read back by a
16
+ // client. The form only ever reads the slot count.
17
+ //
18
+ // Both are wrapped in <EnsureGuest>, which mints an anonymous guest session so
19
+ // the sync connection (which powers the live count) is established. That session
20
+ // holds no PII and can't read the Inquiry table.
21
+
22
+ interface CapacityRow {
23
+ id: string;
24
+ label: string;
25
+ openSlots: number;
26
+ updatedAt: string;
27
+ }
28
+
29
+ function useSeedCapacity() {
30
+ // Create the Capacity row from config on first visit (idempotent server-side).
31
+ useEffect(() => {
32
+ void callFn("seedCapacity", {});
33
+ }, []);
34
+ }
35
+
36
+ /* ------------------------------ hero pill ------------------------------ */
37
+
38
+ export function LiveSlots() {
39
+ return (
40
+ <EnsureGuest fallback={<SlotsPill loading />}>
41
+ <LiveSlotsInner />
42
+ </EnsureGuest>
43
+ );
44
+ }
45
+
46
+ function LiveSlotsInner() {
47
+ useSeedCapacity();
48
+ const { data, loading } = db.useQuery<CapacityRow>("Capacity");
49
+ const row = data[0];
50
+ return <SlotsPill openSlots={row?.openSlots} label={row?.label} live={!loading} />;
51
+ }
52
+
53
+ function SlotsPill({
54
+ openSlots,
55
+ label,
56
+ live,
57
+ loading,
58
+ }: {
59
+ openSlots?: number;
60
+ label?: string;
61
+ live?: boolean;
62
+ loading?: boolean;
63
+ }) {
64
+ const open = openSlots ?? siteConfig.capacity.openSlots;
65
+ const period = label || siteConfig.capacity.label;
66
+ const bookedOut = open <= 0;
67
+ return (
68
+ <span className="inline-flex items-center gap-2 rounded-full border border-zinc-200 bg-white px-3.5 py-1.5 text-[13px] font-medium text-zinc-700 shadow-sm">
69
+ {loading ? (
70
+ <span className="inline-flex size-2 rounded-full bg-zinc-300" />
71
+ ) : (
72
+ <span className="relative flex size-2">
73
+ {live && !bookedOut ? (
74
+ <span className="absolute inline-flex size-2 animate-ping rounded-full bg-green-500/60" />
75
+ ) : null}
76
+ <span
77
+ className={"relative inline-flex size-2 rounded-full " + (bookedOut ? "bg-zinc-400" : "bg-green-600")}
78
+ />
79
+ </span>
80
+ )}
81
+ {bookedOut ? (
82
+ <span>Booked through {period}</span>
83
+ ) : (
84
+ <span>
85
+ <span className="tabular-nums text-zinc-900">{open}</span>{" "}
86
+ {open === 1 ? "project slot" : "project slots"} open · {period}
87
+ </span>
88
+ )}
89
+ </span>
90
+ );
91
+ }
92
+
93
+ /* ----------------------------- contact form ---------------------------- */
94
+
95
+ export function ContactForm() {
96
+ return (
97
+ <EnsureGuest fallback={<FormShell disabled />}>
98
+ <ContactFormInner />
99
+ </EnsureGuest>
100
+ );
101
+ }
102
+
103
+ function ContactFormInner() {
104
+ useSeedCapacity();
105
+ const { data } = db.useQuery<CapacityRow>("Capacity");
106
+ const row = data[0];
107
+ const bookedOut = (row?.openSlots ?? siteConfig.capacity.openSlots) <= 0;
108
+ return <FormShell bookedOut={bookedOut} period={row?.label} />;
109
+ }
110
+
111
+ function FormShell({
112
+ disabled,
113
+ bookedOut,
114
+ period,
115
+ }: {
116
+ disabled?: boolean;
117
+ bookedOut?: boolean;
118
+ period?: string;
119
+ }) {
120
+ const { contact } = siteConfig;
121
+ const [form, setForm] = useState({
122
+ name: "",
123
+ email: "",
124
+ company: "",
125
+ projectType: "",
126
+ budget: "",
127
+ message: "",
128
+ });
129
+ const [status, setStatus] = useState<"idle" | "sending" | "done">("idle");
130
+ const [error, setError] = useState<string | null>(null);
131
+
132
+ const set = (k: keyof typeof form) => (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement>) =>
133
+ setForm((f) => ({ ...f, [k]: e.target.value }));
134
+
135
+ async function onSubmit(e: React.FormEvent) {
136
+ e.preventDefault();
137
+ if (status === "sending") return;
138
+ if (!form.name.trim() || !form.email.trim()) {
139
+ setError("Your name and email are required.");
140
+ return;
141
+ }
142
+ setStatus("sending");
143
+ setError(null);
144
+ try {
145
+ await callFn<{ ok: boolean }>("submitInquiry", {
146
+ name: form.name.trim(),
147
+ email: form.email.trim(),
148
+ company: form.company.trim() || undefined,
149
+ projectType: form.projectType || undefined,
150
+ budget: form.budget || undefined,
151
+ message: form.message.trim() || undefined,
152
+ });
153
+ setStatus("done");
154
+ } catch (err) {
155
+ const msg = err instanceof Error ? err.message : String(err);
156
+ setError(
157
+ /valid email|INVALID_ARGS/i.test(msg)
158
+ ? "Please enter a valid email address."
159
+ : "Something went wrong — try again in a moment.",
160
+ );
161
+ setStatus("idle");
162
+ }
163
+ }
164
+
165
+ if (status === "done") {
166
+ return (
167
+ <div className="rounded-2xl border border-brand/30 bg-brand-soft/50 px-6 py-8 text-center">
168
+ <div className="mx-auto flex size-10 items-center justify-center rounded-full bg-brand text-white">
169
+ <CheckIcon />
170
+ </div>
171
+ <p className="mt-3 text-[15px] font-semibold text-zinc-900">{contact.confirmationMessage}</p>
172
+ </div>
173
+ );
174
+ }
175
+
176
+ return (
177
+ <form onSubmit={onSubmit} className="rounded-2xl border border-zinc-200 bg-white p-6 shadow-sm sm:p-7">
178
+ {bookedOut ? (
179
+ <p className="mb-4 rounded-lg bg-amber-50 px-3 py-2 text-[13px] text-amber-700">
180
+ We&apos;re fully booked{period ? ` through ${period}` : ""} — send a note anyway and we&apos;ll reach out when a slot opens.
181
+ </p>
182
+ ) : null}
183
+ <div className="grid gap-3 sm:grid-cols-2">
184
+ <Field label="Name" required>
185
+ <input value={form.name} onChange={set("name")} autoComplete="name" aria-label="Name" className={inputCls} disabled={disabled} />
186
+ </Field>
187
+ <Field label="Email" required>
188
+ <input type="email" value={form.email} onChange={set("email")} autoComplete="email" aria-label="Email" className={inputCls} disabled={disabled} />
189
+ </Field>
190
+ <Field label="Company">
191
+ <input value={form.company} onChange={set("company")} autoComplete="organization" aria-label="Company" className={inputCls} disabled={disabled} />
192
+ </Field>
193
+ <Field label="Project type">
194
+ <select value={form.projectType} onChange={set("projectType")} aria-label="Project type" className={inputCls} disabled={disabled}>
195
+ <option value="">Select…</option>
196
+ {contact.projectTypes.map((t) => (
197
+ <option key={t} value={t}>{t}</option>
198
+ ))}
199
+ </select>
200
+ </Field>
201
+ </div>
202
+ <div className="mt-3">
203
+ <Field label="Budget">
204
+ <select value={form.budget} onChange={set("budget")} aria-label="Budget" className={inputCls} disabled={disabled}>
205
+ <option value="">Select…</option>
206
+ {contact.budgets.map((b) => (
207
+ <option key={b} value={b}>{b}</option>
208
+ ))}
209
+ </select>
210
+ </Field>
211
+ </div>
212
+ <div className="mt-3">
213
+ <Field label="What are you building?">
214
+ <textarea
215
+ value={form.message}
216
+ onChange={set("message")}
217
+ rows={4}
218
+ aria-label="Message"
219
+ placeholder="A sentence or two about the project, timeline, and what success looks like."
220
+ className={inputCls + " resize-none py-2.5"}
221
+ disabled={disabled}
222
+ />
223
+ </Field>
224
+ </div>
225
+ {error ? <p className="mt-3 text-[13px] text-red-600">{error}</p> : null}
226
+ <button
227
+ type="submit"
228
+ disabled={status === "sending" || disabled}
229
+ className="mt-5 inline-flex h-11 w-full items-center justify-center rounded-full bg-brand text-[15px] font-medium text-white transition-opacity hover:opacity-90 disabled:opacity-60 sm:w-auto sm:px-7"
230
+ >
231
+ {status === "sending" ? "Sending…" : "Send inquiry"}
232
+ </button>
233
+ </form>
234
+ );
235
+ }
236
+
237
+ function Field({ label, required, children }: { label: string; required?: boolean; children: React.ReactNode }) {
238
+ return (
239
+ <label className="block">
240
+ <span className="mb-1.5 block text-[12.5px] font-medium text-zinc-600">
241
+ {label}
242
+ {required ? <span className="text-brand"> *</span> : null}
243
+ </span>
244
+ {children}
245
+ </label>
246
+ );
247
+ }
248
+
249
+ const inputCls =
250
+ "h-10 w-full rounded-lg border border-zinc-300 bg-white px-3 text-[14px] text-zinc-900 outline-none transition placeholder:text-zinc-400 focus:border-brand focus:ring-2 focus:ring-brand/20 disabled:opacity-60";
251
+
252
+ function CheckIcon() {
253
+ return (
254
+ <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="3" strokeLinecap="round" strokeLinejoin="round" aria-hidden>
255
+ <path d="M20 6 9 17l-5-5" />
256
+ </svg>
257
+ );
258
+ }