@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,160 @@
1
+ import React from "react";
2
+ import { Link, type PageAuth } from "@pylonsync/react";
3
+ import { siteConfig } from "@/lib/site.config";
4
+ import { SectionScroller } from "@/components/section-scroller";
5
+
6
+ // A layout wraps every page. This marketing layout renders a slim nav up top
7
+ // and a footer below, both driven by lib/site.config.ts. `auth.user_id` is
8
+ // resolved server-side from the session cookie before any HTML is sent, so the
9
+ // nav shows "Dashboard" once the owner is signed in and "Sign in" otherwise —
10
+ // no flash, no client fetch.
11
+ interface LayoutProps {
12
+ children: React.ReactNode;
13
+ url: string;
14
+ auth: PageAuth;
15
+ }
16
+
17
+ export default function RootLayout({ children, url, auth }: LayoutProps) {
18
+ // A guest session (minted by <EnsureGuest> for the live counter) has a
19
+ // `guest_…` user id — that's an anonymous visitor, NOT the signed-in owner,
20
+ // so it shouldn't flip the nav to "Dashboard".
21
+ const signedIn = Boolean(auth?.user_id && !auth.user_id.startsWith("guest_"));
22
+ const { brand, colors } = siteConfig;
23
+
24
+ // The auth screens and the dashboard bring their own chrome, so they render
25
+ // bare (no marketing nav/footer). Match on the path PREFIX, not a substring.
26
+ const path = (url ?? "").split("?")[0];
27
+ const BARE_PREFIXES = ["/login", "/dashboard"];
28
+ const isBare = BARE_PREFIXES.some((p) => path === p || path.startsWith(p + "/"));
29
+
30
+ return (
31
+ <html
32
+ lang="en"
33
+ // Marketing theme colors come from the single site config. Set as inline
34
+ // CSS vars on <html> so they override globals.css and the whole page
35
+ // re-themes from one place — no CSS edit needed.
36
+ style={
37
+ {
38
+ "--brand": colors.brand,
39
+ "--brand-soft": colors.brandSoft,
40
+ "--paper": colors.paper,
41
+ } as React.CSSProperties
42
+ }
43
+ >
44
+ <head>
45
+ <meta charSet="utf-8" />
46
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
47
+ {/* No <title> here — each page's exported `metadata` sets it. */}
48
+ <link rel="preconnect" href="https://fonts.googleapis.com" />
49
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossOrigin="anonymous" />
50
+ <link
51
+ rel="stylesheet"
52
+ href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap"
53
+ />
54
+ {/* Tailwind is compiled by Pylon from app/globals.css and injected here. */}
55
+ </head>
56
+ <body className="flex min-h-screen flex-col bg-background text-foreground antialiased">
57
+ <SectionScroller />
58
+ {isBare ? (
59
+ children
60
+ ) : (
61
+ <>
62
+ <header className="sticky top-0 z-30 bg-white/80 backdrop-blur">
63
+ <div className="mx-auto flex h-14 max-w-3xl items-center justify-between px-6">
64
+ <Link href="/" className="flex items-center gap-2">
65
+ <span className="flex size-6 items-center justify-center rounded-[7px] bg-zinc-900 text-[13px] font-bold text-white">
66
+ {brand.letter}
67
+ </span>
68
+ <span className="text-[15px] font-semibold tracking-tight text-zinc-900">
69
+ {brand.name}
70
+ </span>
71
+ </Link>
72
+ <nav className="flex items-center gap-2">
73
+ {signedIn ? (
74
+ <Link
75
+ href="/dashboard"
76
+ className="inline-flex items-center rounded-full bg-zinc-900 px-3.5 py-1.5 text-[13px] font-medium text-white transition-colors hover:bg-zinc-700"
77
+ >
78
+ Dashboard
79
+ </Link>
80
+ ) : (
81
+ <Link
82
+ href="/login"
83
+ className="rounded-full px-3 py-1.5 text-[13px] font-medium text-zinc-600 transition-colors hover:text-zinc-900"
84
+ >
85
+ Sign in
86
+ </Link>
87
+ )}
88
+ </nav>
89
+ </div>
90
+ </header>
91
+
92
+ <main className="flex-1">{children}</main>
93
+
94
+ <SiteFooter />
95
+ </>
96
+ )}
97
+ </body>
98
+ </html>
99
+ );
100
+ }
101
+
102
+ function SiteFooter() {
103
+ const { brand } = siteConfig;
104
+ return (
105
+ <footer className="border-t border-zinc-200/70 bg-white">
106
+ <div className="mx-auto max-w-3xl px-6 py-12">
107
+ <div className="flex flex-col items-start justify-between gap-6 sm:flex-row">
108
+ <div className="max-w-sm">
109
+ <Link href="/" className="inline-flex items-center gap-2">
110
+ <span className="flex size-6 items-center justify-center rounded-[7px] bg-zinc-900 text-[13px] font-bold text-white">
111
+ {brand.letter}
112
+ </span>
113
+ <span className="text-[15px] font-semibold tracking-tight text-zinc-900">
114
+ {brand.name}
115
+ </span>
116
+ </Link>
117
+ <p className="mt-3 text-[13px] leading-relaxed text-zinc-500">
118
+ {brand.footerBlurb}
119
+ </p>
120
+ </div>
121
+ <div className="flex items-center gap-4">
122
+ {brand.socials.map((s) => (
123
+ <a
124
+ key={s.label}
125
+ href={s.href}
126
+ aria-label={s.label}
127
+ className="text-zinc-400 transition-colors hover:text-zinc-900"
128
+ >
129
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor">
130
+ <path d={s.path} />
131
+ </svg>
132
+ </a>
133
+ ))}
134
+ <a
135
+ href={`mailto:${brand.email}`}
136
+ aria-label="Email"
137
+ className="text-zinc-400 transition-colors hover:text-zinc-900"
138
+ >
139
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
140
+ <rect x="3" y="5" width="18" height="14" rx="2" />
141
+ <path d="m3 7 9 6 9-6" />
142
+ </svg>
143
+ </a>
144
+ </div>
145
+ </div>
146
+ <div className="mt-10 flex flex-col items-start justify-between gap-3 border-t border-zinc-200/70 pt-6 text-[12px] text-zinc-400 sm:flex-row sm:items-center">
147
+ <span>
148
+ © {new Date().getFullYear()} {brand.copyrightName}
149
+ </span>
150
+ <span>
151
+ Built with{" "}
152
+ <a href="https://pylonsync.com" className="font-medium text-zinc-600 hover:text-zinc-900">
153
+ Pylon
154
+ </a>
155
+ </span>
156
+ </div>
157
+ </div>
158
+ </footer>
159
+ );
160
+ }
@@ -0,0 +1,39 @@
1
+ import React from "react";
2
+ import { Link, type Metadata, type PageProps } from "@pylonsync/react";
3
+ import { AuthForm } from "../auth-form";
4
+ import { siteConfig } from "@/lib/site.config";
5
+
6
+ export const metadata: Metadata = {
7
+ title: `Sign in — ${siteConfig.brand.name}`,
8
+ robots: "noindex",
9
+ };
10
+
11
+ // `app/login/page.tsx` → `/login`. The owner's sign-in. Rendered bare (the
12
+ // layout suppresses the marketing nav/footer for /login). Already signed in?
13
+ // Skip straight to the dashboard — `response.redirect` in the synchronous shell
14
+ // render is a real 307 before any HTML is sent.
15
+ export default function LoginPage({ auth, response }: PageProps) {
16
+ // Already signed in (a real account, not an anonymous guest)? Skip the form.
17
+ if (auth.user_id && !auth.user_id.startsWith("guest_")) response.redirect("/dashboard");
18
+ const { brand } = siteConfig;
19
+ return (
20
+ <div className="flex min-h-screen items-center justify-center bg-white px-6 py-12">
21
+ <div className="w-full max-w-[400px] rounded-2xl border border-zinc-200/70 p-8">
22
+ <Link href="/" className="inline-flex">
23
+ <span className="flex size-9 items-center justify-center rounded-xl bg-zinc-900 text-base font-bold text-white">
24
+ {brand.letter}
25
+ </span>
26
+ </Link>
27
+ <h1 className="mt-5 text-[22px] font-semibold tracking-tight text-zinc-900">
28
+ {brand.name} dashboard
29
+ </h1>
30
+ <p className="mt-1 text-[13px] text-zinc-500">
31
+ Sign in to see your subscribers.
32
+ </p>
33
+ <div className="mt-6">
34
+ <AuthForm />
35
+ </div>
36
+ </div>
37
+ </div>
38
+ );
39
+ }
@@ -0,0 +1,162 @@
1
+ "use client";
2
+
3
+ import React, { useEffect, useRef, useState } from "react";
4
+ import { db, callFn } from "@pylonsync/react";
5
+ import { EnsureGuest } from "@pylonsync/client";
6
+ import type { CreatorConfig } from "@/lib/site.config";
7
+
8
+ // The newsletter signup — email capture plus a LIVE subscriber counter. This is
9
+ // the realtime proof: the counter is a live `db.useQuery("SubscriberCount")`
10
+ // over the public, PII-free aggregate row, so the moment anyone (this tab or
11
+ // another) subscribes, `subscribe` updates that row and the new count syncs to
12
+ // every open tab. No refresh.
13
+ //
14
+ // The form (subscribe) is a public mutation, so it works for any anonymous
15
+ // visitor. The counter needs a live sync connection, so it's wrapped in
16
+ // <EnsureGuest>, which mints an anonymous guest session. It holds no PII, and
17
+ // the Subscriber table stays unreadable to it — only the aggregate count leaves.
18
+
19
+ type Props = { newsletter: CreatorConfig["newsletter"] };
20
+
21
+ export function NewsletterSignup({ newsletter }: Props) {
22
+ return (
23
+ <div>
24
+ <SubscribeForm newsletter={newsletter} />
25
+ <div className="mt-5">
26
+ <LiveCounter seed={newsletter.seedCount ?? 0} label={newsletter.counterLabel} />
27
+ </div>
28
+ </div>
29
+ );
30
+ }
31
+
32
+ function SubscribeForm({ newsletter }: Props) {
33
+ const [email, setEmail] = useState("");
34
+ const [status, setStatus] = useState<"idle" | "sending" | "done">("idle");
35
+ const [alreadyJoined, setAlreadyJoined] = useState(false);
36
+ const [error, setError] = useState<string | null>(null);
37
+
38
+ async function onSubmit(e: React.FormEvent) {
39
+ e.preventDefault();
40
+ const value = email.trim();
41
+ if (!value || status === "sending") return;
42
+ setStatus("sending");
43
+ setError(null);
44
+ try {
45
+ const res = await callFn<{ ok: boolean; alreadyJoined: boolean }>("subscribe", {
46
+ email: value,
47
+ });
48
+ setAlreadyJoined(res.alreadyJoined);
49
+ setStatus("done");
50
+ } catch (err) {
51
+ const msg = err instanceof Error ? err.message : String(err);
52
+ setError(
53
+ /valid email|INVALID_ARGS/i.test(msg)
54
+ ? "Enter a valid email address."
55
+ : "Something went wrong — try again in a moment.",
56
+ );
57
+ setStatus("idle");
58
+ }
59
+ }
60
+
61
+ if (status === "done") {
62
+ return (
63
+ <div className="rounded-xl border border-brand/30 bg-brand-soft/50 px-5 py-4">
64
+ <p className="text-[15px] font-medium text-zinc-900">
65
+ {alreadyJoined ? "You're already subscribed." : newsletter.successMessage}
66
+ </p>
67
+ </div>
68
+ );
69
+ }
70
+
71
+ return (
72
+ <form onSubmit={onSubmit} className="max-w-md">
73
+ <div className="flex flex-col gap-2 sm:flex-row">
74
+ <input
75
+ type="email"
76
+ inputMode="email"
77
+ autoComplete="email"
78
+ value={email}
79
+ onChange={(e) => setEmail(e.target.value)}
80
+ placeholder={newsletter.emailPlaceholder}
81
+ aria-label="Email address"
82
+ required
83
+ className="h-11 flex-1 rounded-full border border-zinc-300 bg-white px-4 text-[15px] text-zinc-900 outline-none transition placeholder:text-zinc-400 focus:border-brand focus:ring-2 focus:ring-brand/20"
84
+ />
85
+ <button
86
+ type="submit"
87
+ disabled={status === "sending"}
88
+ className="inline-flex h-11 items-center justify-center rounded-full bg-brand px-6 text-[15px] font-medium text-white transition-opacity hover:opacity-90 disabled:opacity-60"
89
+ >
90
+ {status === "sending" ? "Subscribing…" : newsletter.ctaLabel}
91
+ </button>
92
+ </div>
93
+ {error ? (
94
+ <p className="mt-2 text-[13px] text-red-600">{error}</p>
95
+ ) : (
96
+ <p className="mt-2 text-[13px] text-zinc-400">One email a week. Unsubscribe anytime.</p>
97
+ )}
98
+ </form>
99
+ );
100
+ }
101
+
102
+ interface SubscriberCountRow {
103
+ id: string;
104
+ count: number;
105
+ }
106
+
107
+ function LiveCounter({ seed, label }: { seed: number; label: string }) {
108
+ return (
109
+ <EnsureGuest fallback={<CounterView value={seed} label={label} />}>
110
+ <LiveCounterInner seed={seed} label={label} />
111
+ </EnsureGuest>
112
+ );
113
+ }
114
+
115
+ function LiveCounterInner({ seed, label }: { seed: number; label: string }) {
116
+ const { data, loading } = db.useQuery<SubscriberCountRow>("SubscriberCount");
117
+ const real = data.length > 0 ? data[0].count : 0;
118
+ return <CounterView value={seed + real} label={label} live={!loading} />;
119
+ }
120
+
121
+ function CounterView({ value, label, live }: { value: number; label: string; live?: boolean }) {
122
+ const shown = useCountUp(value);
123
+ return (
124
+ <div className="inline-flex items-center gap-2.5 text-[14px] text-zinc-500">
125
+ {live ? (
126
+ <span className="relative flex size-2">
127
+ <span className="absolute inline-flex size-2 animate-ping rounded-full bg-brand/70" />
128
+ <span className="relative inline-flex size-2 rounded-full bg-brand" />
129
+ </span>
130
+ ) : (
131
+ <span className="inline-flex size-2 rounded-full bg-zinc-300" />
132
+ )}
133
+ <span className="font-semibold tabular-nums text-zinc-900">{shown.toLocaleString()}</span>
134
+ {label}
135
+ </div>
136
+ );
137
+ }
138
+
139
+ function useCountUp(target: number): number {
140
+ const [shown, setShown] = useState(target);
141
+ const fromRef = useRef(target);
142
+ const rafRef = useRef<number | null>(null);
143
+ useEffect(() => {
144
+ const from = fromRef.current;
145
+ if (from === target) return;
146
+ const start = performance.now();
147
+ const dur = 600;
148
+ const step = (now: number) => {
149
+ const t = Math.min(1, (now - start) / dur);
150
+ const eased = 1 - Math.pow(1 - t, 3);
151
+ setShown(Math.round(from + (target - from) * eased));
152
+ if (t < 1) rafRef.current = requestAnimationFrame(step);
153
+ else fromRef.current = target;
154
+ };
155
+ rafRef.current = requestAnimationFrame(step);
156
+ return () => {
157
+ if (rafRef.current != null) cancelAnimationFrame(rafRef.current);
158
+ fromRef.current = target;
159
+ };
160
+ }, [target]);
161
+ return shown;
162
+ }
@@ -0,0 +1,19 @@
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()`). Hydrated, so the link is a client nav.
6
+ export default function NotFound(_props: NotFoundProps) {
7
+ return (
8
+ <div className="mx-auto flex min-h-[60vh] max-w-3xl flex-col items-center justify-center px-6 text-center">
9
+ <h1 className="text-3xl font-semibold tracking-tight">404</h1>
10
+ <p className="mt-2 text-zinc-500">We couldn&apos;t find that page.</p>
11
+ <Link
12
+ href="/"
13
+ className="mt-6 inline-flex h-10 items-center rounded-full bg-zinc-900 px-5 text-sm font-medium text-white transition-colors hover:bg-zinc-700"
14
+ >
15
+ Back home
16
+ </Link>
17
+ </div>
18
+ );
19
+ }
@@ -0,0 +1,160 @@
1
+ import React from "react";
2
+ import { type Metadata } from "@pylonsync/react";
3
+ import {
4
+ WRAP,
5
+ Eyebrow,
6
+ Divider,
7
+ SectionHead,
8
+ initials,
9
+ ImagePlaceholder,
10
+ } from "@/components/marketing";
11
+ import { NewsletterSignup } from "./newsletter-signup";
12
+ import { siteConfig } from "@/lib/site.config";
13
+
14
+ export const metadata: Metadata = {
15
+ title: siteConfig.seo.title,
16
+ description: siteConfig.seo.description,
17
+ openGraph: { title: siteConfig.seo.title, description: siteConfig.seo.description, type: "website" },
18
+ };
19
+
20
+ // `app/page.tsx` → `/`. A server-rendered personal-brand site. Hero, about,
21
+ // offerings, testimonials, and links are static server HTML (SEO + first paint);
22
+ // the newsletter section (#newsletter) is a client island with a live
23
+ // subscriber counter. All copy comes from siteConfig. Doesn't read `auth`, so
24
+ // the public page stays cacheable.
25
+ export default function LandingPage() {
26
+ const { hero, about, offerings, testimonials, newsletter, links } = siteConfig;
27
+
28
+ return (
29
+ <div className="bg-white text-zinc-900">
30
+ {/* HERO */}
31
+ <section className={`${WRAP} pt-16 pb-14 sm:pt-20`}>
32
+ <div className="grid items-center gap-10 sm:grid-cols-[1fr_auto]">
33
+ <div>
34
+ <span className="inline-flex items-center gap-2 rounded-full border border-zinc-200 bg-white py-1 pl-1.5 pr-3 text-[13px] text-zinc-600 shadow-sm">
35
+ <span className="inline-block size-1.5 rounded-full bg-brand" />
36
+ {hero.badge}
37
+ </span>
38
+ <h1 className="mt-5 text-balance text-[2.5rem] font-semibold leading-[1.04] tracking-[-0.02em] sm:text-[3rem]">
39
+ {hero.name}
40
+ </h1>
41
+ <p className="mt-2 text-[18px] font-medium text-brand">{hero.tagline}</p>
42
+ <p className="mt-5 max-w-xl text-[16px] leading-relaxed text-zinc-500">{hero.intro}</p>
43
+ <div className="mt-7 flex flex-wrap items-center gap-4">
44
+ <a href="#newsletter" className="inline-flex items-center rounded-full bg-zinc-900 px-5 py-2.5 text-sm font-medium text-white transition-colors hover:bg-zinc-700">
45
+ Read the newsletter
46
+ </a>
47
+ <a href="#work" className="text-sm font-medium text-zinc-700 hover:text-zinc-900">
48
+ Work with me →
49
+ </a>
50
+ </div>
51
+ </div>
52
+ {/* Profile photo — replace the placeholder with a real headshot. */}
53
+ <div className="order-first w-32 sm:order-none sm:w-44">
54
+ <ImagePlaceholder
55
+ shape="square"
56
+ title="Your headshot"
57
+ hint="Replace in app/page.tsx"
58
+ />
59
+ </div>
60
+ </div>
61
+ </section>
62
+
63
+ {/* ABOUT */}
64
+ <Divider />
65
+ <section className={`${WRAP} py-14`}>
66
+ <div className="grid gap-8 lg:grid-cols-[0.8fr_1.2fr]">
67
+ <SectionHead eyebrow={about.eyebrow} title={about.headline} />
68
+ <div className="space-y-4 text-[15px] leading-relaxed text-zinc-600">
69
+ {about.paragraphs.map((p) => (
70
+ <p key={p}>{p}</p>
71
+ ))}
72
+ </div>
73
+ </div>
74
+ </section>
75
+
76
+ {/* OFFERINGS */}
77
+ <Divider />
78
+ <section id="work" className={`${WRAP} py-14`}>
79
+ <SectionHead eyebrow={offerings.eyebrow} title={offerings.headline} />
80
+ <div className="mt-8 grid gap-5 sm:grid-cols-3">
81
+ {offerings.items.map((o) => (
82
+ <div key={o.title} className="flex flex-col rounded-2xl border border-zinc-200 bg-paper p-6">
83
+ <h3 className="text-[15px] font-semibold text-zinc-900">{o.title}</h3>
84
+ <p className="mt-2 flex-1 text-[14px] leading-relaxed text-zinc-500">{o.body}</p>
85
+ {o.price ? (
86
+ <p className="mt-4 text-[13px] font-medium uppercase tracking-wide text-brand">{o.price}</p>
87
+ ) : null}
88
+ </div>
89
+ ))}
90
+ </div>
91
+ </section>
92
+
93
+ {/* TESTIMONIALS */}
94
+ {testimonials && testimonials.items.length > 0 ? (
95
+ <>
96
+ <Divider />
97
+ <section className={`${WRAP} py-14`}>
98
+ <SectionHead eyebrow={testimonials.eyebrow} title={testimonials.headline} />
99
+ <div className="mt-8 grid gap-5 sm:grid-cols-3">
100
+ {testimonials.items.map((t) => (
101
+ <figure key={t.name} className="flex flex-col rounded-2xl border border-zinc-200 bg-paper p-6">
102
+ <blockquote className="text-[14px] leading-relaxed text-zinc-600">&ldquo;{t.quote}&rdquo;</blockquote>
103
+ <figcaption className="mt-5 flex items-center gap-3">
104
+ <span className="flex size-8 items-center justify-center rounded-full bg-zinc-200 text-[11px] font-semibold text-zinc-500">
105
+ {initials(t.name)}
106
+ </span>
107
+ <div className="leading-tight">
108
+ <div className="text-[13px] font-semibold">{t.name}</div>
109
+ <div className="text-[12px] text-zinc-500">{t.role}</div>
110
+ </div>
111
+ </figcaption>
112
+ </figure>
113
+ ))}
114
+ </div>
115
+ </section>
116
+ </>
117
+ ) : null}
118
+
119
+ {/* NEWSLETTER (live) */}
120
+ <Divider />
121
+ <section id="newsletter" className={`${WRAP} py-14`}>
122
+ <div className="rounded-2xl border border-zinc-200 bg-paper p-7 sm:p-10">
123
+ <Eyebrow>{newsletter.eyebrow}</Eyebrow>
124
+ <h2 className="mt-4 max-w-2xl text-balance text-2xl font-semibold leading-[1.15] tracking-[-0.02em] sm:text-3xl">
125
+ {newsletter.headline}
126
+ </h2>
127
+ <p className="mt-4 max-w-xl text-[15px] leading-relaxed text-zinc-500">{newsletter.subcopy}</p>
128
+ <div className="mt-7">
129
+ <NewsletterSignup newsletter={newsletter} />
130
+ </div>
131
+ </div>
132
+ </section>
133
+
134
+ {/* LINKS */}
135
+ {links && links.items.length > 0 ? (
136
+ <>
137
+ <Divider />
138
+ <section className={`${WRAP} py-14`}>
139
+ <SectionHead eyebrow={links.eyebrow} title={links.headline} />
140
+ <div className="mt-8 grid gap-3 sm:grid-cols-3">
141
+ {links.items.map((l) => (
142
+ <a
143
+ key={l.label}
144
+ href={l.href}
145
+ className="group rounded-xl border border-zinc-200 bg-white px-5 py-4 transition-colors hover:border-zinc-300"
146
+ >
147
+ <div className="flex items-center justify-between text-[14px] font-medium text-zinc-900">
148
+ {l.label}
149
+ <span className="text-zinc-300 transition-colors group-hover:text-brand">→</span>
150
+ </div>
151
+ {l.note ? <p className="mt-1 text-[12.5px] text-zinc-500">{l.note}</p> : null}
152
+ </a>
153
+ ))}
154
+ </div>
155
+ </section>
156
+ </>
157
+ ) : null}
158
+ </div>
159
+ );
160
+ }
@@ -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
+ // Keep the owner dashboard and the API out of the index.
9
+ rules: { userAgent: "*", allow: "/", disallow: ["/dashboard", "/login", "/api/"] },
10
+ sitemap: `${SITE}/sitemap.xml`,
11
+ };
12
+ }
@@ -0,0 +1,9 @@
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. The newsletter is a single public page, so the sitemap is just "/".
5
+ const SITE = process.env.SITE_URL ?? "http://localhost:4321";
6
+
7
+ export default async function sitemap(): Promise<Sitemap> {
8
+ return [{ url: `${SITE}/`, changeFrequency: "weekly", priority: 1 }];
9
+ }