@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,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
+ }
@@ -0,0 +1,134 @@
1
+ import {
2
+ entity,
3
+ field,
4
+ policy,
5
+ auth,
6
+ buildManifest,
7
+ discoverAppRoutes,
8
+ } from "@pylonsync/sdk";
9
+
10
+ // ---------------------------------------------------------------------------
11
+ // newsletter — a pre-launch / coming-soon landing page with a LIVE subscriber
12
+ // counter. The whole point is the realtime hook: open the page in two tabs,
13
+ // submit an email in one, and the counter on the other ticks up with no
14
+ // refresh. That's the proof it's a real live app and not a static page.
15
+ //
16
+ // The data model is deliberately tiny — two entities:
17
+ // • Subscriber — one row per email. Holds visitor PII, so it denies ALL client
18
+ // reads/writes (writes go through the subscribe mutation; the
19
+ // public page only ever sees an aggregate count, never an email).
20
+ // • User — the business owner's account (email/password is built in), so
21
+ // the owner can sign in to the dashboard and see their subscribers.
22
+ // ---------------------------------------------------------------------------
23
+
24
+ // One newsletter subscriber. `email` is the only PII; `createdAt` powers the
25
+ // subscribers-over-time chart on the dashboard. The unique index on email dedupes
26
+ // at the database level — a duplicate insert is rejected even under a race, so
27
+ // subscribe can treat the conflict as "already joined".
28
+ const Subscriber = entity(
29
+ "Subscriber",
30
+ {
31
+ email: field.string(),
32
+ createdAt: field.datetime().defaultNow(),
33
+ },
34
+ {
35
+ indexes: [
36
+ { name: "by_email", fields: ["email"], unique: true },
37
+ { name: "by_created", fields: ["createdAt"], unique: false },
38
+ ],
39
+ },
40
+ );
41
+
42
+ // A single-row, PII-FREE aggregate the public page can safely read live. It
43
+ // holds only the subscriber count — no emails. `subscribe` keeps it in sync with
44
+ // the real Subscriber count on every new join. The landing page subscribes with
45
+ // `db.useQuery("SubscriberCount")`, which syncs across every open tab through the
46
+ // replica — so the counter ticks up everywhere the instant someone joins. This
47
+ // is the cross-tab-safe realtime primitive (entity sync), not a per-connection
48
+ // server-query subscription.
49
+ const SubscriberCount = entity(
50
+ "SubscriberCount",
51
+ {
52
+ count: field.int().default(0),
53
+ updatedAt: field.datetime().defaultNow(),
54
+ },
55
+ {},
56
+ );
57
+
58
+ // The business owner's account. Email/password auth is built in against an
59
+ // entity named "User" (passwordHash is server-only; the register route stamps
60
+ // avatarColor). The dashboard is gated to the owner — see PYLON_OWNER_EMAIL in
61
+ // functions/subscriberStats.ts and app/dashboard/page.tsx.
62
+ const User = entity(
63
+ "User",
64
+ {
65
+ email: field.string(),
66
+ displayName: field.string().optional(),
67
+ passwordHash: field.string().serverOnly().optional(),
68
+ avatarColor: field.string().optional(),
69
+ // Set when the owner verifies their email; unused by the newsletter flow but
70
+ // declared so the framework's email-verification routes have a column.
71
+ emailVerified: field.datetime().optional(),
72
+ createdAt: field.datetime().defaultNow(),
73
+ },
74
+ { indexes: [{ name: "by_email", fields: ["email"], unique: true }] },
75
+ );
76
+
77
+ // PRIVACY — the heart of the spec. Subscriber holds visitor emails, so it denies
78
+ // EVERY client read and write. No `db.useQuery("Subscriber")` can ever pull a row,
79
+ // and no client can insert/update/delete directly. Writes happen only inside
80
+ // the server-side `subscribe` mutation (functions bypass policies); reads
81
+ // happen only inside `newsletterCount` (returns a bare integer) and the
82
+ // owner-gated `subscriberStats`. A marketing site must never leak its own
83
+ // customers' emails — this policy is what guarantees it.
84
+ const subscriberPolicy = policy({
85
+ name: "subscriber_private",
86
+ entity: "Subscriber",
87
+ allowRead: "false",
88
+ allowInsert: "false",
89
+ allowUpdate: "false",
90
+ allowDelete: "false",
91
+ });
92
+
93
+ // The aggregate count is public to READ (it's just a number — the whole point
94
+ // is that the landing page shows it live to everyone). Clients can't WRITE it;
95
+ // only the subscribe mutation maintains it server-side.
96
+ const subscriberCountPolicy = policy({
97
+ name: "subscriber_count_public_read",
98
+ entity: "SubscriberCount",
99
+ allowRead: "true",
100
+ allowInsert: "false",
101
+ allowUpdate: "false",
102
+ allowDelete: "false",
103
+ });
104
+
105
+ // The owner reads their own User row (the dashboard resolves their email this
106
+ // way to check ownership). The auth subsystem owns all writes.
107
+ const userPolicy = policy({
108
+ name: "user_self",
109
+ entity: "User",
110
+ allowRead: "auth.userId == data.id",
111
+ allowInsert: "false",
112
+ allowUpdate: "false",
113
+ allowDelete: "false",
114
+ });
115
+
116
+ const manifest = buildManifest({
117
+ name: "__APP_NAME__",
118
+ version: "0.1.0",
119
+ entities: [Subscriber, SubscriberCount, User],
120
+ // subscribe (public mutation) + subscriberStats (owner-gated query) live in
121
+ // functions/ and are discovered automatically — they don't need listing here.
122
+ queries: [],
123
+ actions: [],
124
+ policies: [subscriberPolicy, subscriberCountPolicy, userPolicy],
125
+ // Email/password is on by default against the User entity above. No orgs,
126
+ // no billing — a newsletter is single-tenant (one business, one owner).
127
+ auth: auth(),
128
+ routes: await discoverAppRoutes(),
129
+ });
130
+
131
+ // Emit the canonical manifest JSON to stdout — `pylon dev` captures this.
132
+ console.log(JSON.stringify(manifest, null, 2));
133
+
134
+ export default manifest;
@@ -0,0 +1,148 @@
1
+ import React from "react";
2
+
3
+ // Reusable presentational pieces for the landing page. All server-rendered —
4
+ // no client JS. Restyle here and the whole page follows. The brand accent
5
+ // (`text-brand`, `bg-brand-soft`) comes from CSS vars set on <html> in
6
+ // app/layout.tsx, which read lib/site.config.ts — so re-theming is one edit.
7
+
8
+ // Shared container: a contained, centered column.
9
+ export const WRAP = "mx-auto w-full max-w-3xl px-6";
10
+
11
+ export function Eyebrow({ children }: { children: React.ReactNode }) {
12
+ return (
13
+ <p className="font-mono text-[11px] font-semibold uppercase tracking-[0.14em] text-brand">
14
+ {children}
15
+ </p>
16
+ );
17
+ }
18
+
19
+ // "New / Coming soon"-style pill for the hero.
20
+ export function Badge({ children }: { children: React.ReactNode }) {
21
+ return (
22
+ <span className="inline-flex items-center gap-2 rounded-full border border-zinc-200 bg-white py-1 pl-1.5 pr-3 text-[13px] text-zinc-600 shadow-sm">
23
+ <span className="inline-block size-1.5 rounded-full bg-brand" />
24
+ {children}
25
+ </span>
26
+ );
27
+ }
28
+
29
+ export function Divider() {
30
+ return (
31
+ <div className={WRAP}>
32
+ <div className="border-t border-zinc-200/70" />
33
+ </div>
34
+ );
35
+ }
36
+
37
+ export function SectionHead({
38
+ eyebrow,
39
+ title,
40
+ body,
41
+ }: {
42
+ eyebrow: string;
43
+ title: string;
44
+ body?: string;
45
+ }) {
46
+ return (
47
+ <div>
48
+ <Eyebrow>{eyebrow}</Eyebrow>
49
+ <h2 className="mt-4 text-balance text-2xl font-semibold leading-[1.15] tracking-[-0.02em] sm:text-3xl">
50
+ {title}
51
+ </h2>
52
+ {body ? (
53
+ <p className="mt-4 max-w-xl text-[15px] leading-relaxed text-zinc-500">
54
+ {body}
55
+ </p>
56
+ ) : null}
57
+ </div>
58
+ );
59
+ }
60
+
61
+ // A grid of value props — icon + title + body.
62
+ export function FeatureGrid({
63
+ items,
64
+ }: {
65
+ items: { title: string; body: string; icon?: string }[];
66
+ }) {
67
+ return (
68
+ <div className="grid gap-6 sm:grid-cols-3">
69
+ {items.map((f) => (
70
+ <div key={f.title}>
71
+ {f.icon ? (
72
+ <span className="flex size-9 items-center justify-center rounded-lg bg-brand-soft text-brand">
73
+ {f.icon}
74
+ </span>
75
+ ) : null}
76
+ <h3 className="mt-4 text-[15px] font-semibold text-zinc-900">
77
+ {f.title}
78
+ </h3>
79
+ <p className="mt-2 text-[14px] leading-relaxed text-zinc-500">
80
+ {f.body}
81
+ </p>
82
+ </div>
83
+ ))}
84
+ </div>
85
+ );
86
+ }
87
+
88
+ // Initials for testimonial avatars, so the cards look finished without a photo.
89
+ export function initials(name: string) {
90
+ return name
91
+ .split(/\s+/)
92
+ .map((w) => w[0])
93
+ .join("")
94
+ .slice(0, 2)
95
+ .toUpperCase();
96
+ }
97
+
98
+ // A deliberately-obvious image placeholder. Real sites drop a photo here; this
99
+ // makes the spot unmistakable — dashed border, a photo glyph, and a one-line
100
+ // "swap this" instruction telling you exactly what to replace and where. Looks
101
+ // tidy enough to demo, but no one will mistake it for a finished design.
102
+ //
103
+ // shape — "landscape" | "portrait" | "square" | "circle"
104
+ // title — what photo belongs here ("Your headshot")
105
+ // hint — how to replace it ("Replace in app/page.tsx")
106
+ export function ImagePlaceholder({
107
+ shape = "landscape",
108
+ title,
109
+ hint,
110
+ className = "",
111
+ }: {
112
+ shape?: "landscape" | "portrait" | "square" | "circle";
113
+ title: string;
114
+ hint?: string;
115
+ className?: string;
116
+ }) {
117
+ const aspect =
118
+ shape === "portrait"
119
+ ? "aspect-[4/5]"
120
+ : shape === "square" || shape === "circle"
121
+ ? "aspect-square"
122
+ : "aspect-[4/3]";
123
+ const radius = shape === "circle" ? "rounded-full" : "rounded-2xl";
124
+ return (
125
+ <div
126
+ className={`relative grid place-items-center overflow-hidden border-2 border-dashed border-zinc-300 bg-zinc-50 ${aspect} ${radius} ${className}`}
127
+ >
128
+ <div className="px-4 text-center">
129
+ <svg
130
+ className="mx-auto size-7 text-zinc-300"
131
+ viewBox="0 0 24 24"
132
+ fill="none"
133
+ stroke="currentColor"
134
+ strokeWidth="1.5"
135
+ strokeLinecap="round"
136
+ strokeLinejoin="round"
137
+ aria-hidden
138
+ >
139
+ <rect x="3" y="3" width="18" height="18" rx="2" />
140
+ <circle cx="9" cy="9" r="1.6" />
141
+ <path d="m21 15-4.5-4.5L7 20" />
142
+ </svg>
143
+ <p className="mt-2 text-[12.5px] font-medium text-zinc-500">{title}</p>
144
+ {hint ? <p className="mt-1 text-[11px] leading-snug text-zinc-400">{hint}</p> : null}
145
+ </div>
146
+ </div>
147
+ );
148
+ }
@@ -0,0 +1,35 @@
1
+ "use client";
2
+
3
+ import { useEffect } from "react";
4
+
5
+ // Makes in-page section links work. A hydrated Pylon page updates the URL for a
6
+ // plain `<a href="#section">` click but doesn't perform the browser's native
7
+ // fragment scroll, so the page jumps nowhere. This installs ONE delegated click
8
+ // handler that catches any same-page `#`/`/#` anchor and scrolls to it smoothly.
9
+ //
10
+ // Render it once (in the root layout). Renders nothing. Real route links should
11
+ // still use `<Link>` from @pylonsync/react — this only handles `#` anchors.
12
+ export function SectionScroller() {
13
+ useEffect(() => {
14
+ function onClick(e: MouseEvent) {
15
+ if (e.defaultPrevented || e.button !== 0 || e.metaKey || e.ctrlKey || e.shiftKey || e.altKey) {
16
+ return;
17
+ }
18
+ const target = e.target as Element | null;
19
+ const link = target?.closest?.('a[href^="#"], a[href^="/#"]') as HTMLAnchorElement | null;
20
+ if (!link) return;
21
+ const href = link.getAttribute("href") || "";
22
+ const id = href.slice(href.indexOf("#") + 1);
23
+ if (!id) return;
24
+ const el = document.getElementById(id);
25
+ if (!el) return; // target not on this page — leave it to the browser
26
+ e.preventDefault();
27
+ el.scrollIntoView({ block: "start" });
28
+ history.replaceState(null, "", "#" + id);
29
+ }
30
+ document.addEventListener("click", onClick);
31
+ return () => document.removeEventListener("click", onClick);
32
+ }, []);
33
+
34
+ return null;
35
+ }
@@ -0,0 +1,56 @@
1
+ import * as React from "react";
2
+ import { Slot } from "@radix-ui/react-slot";
3
+ import { cva, type VariantProps } from "class-variance-authority";
4
+
5
+ import { cn } from "@/lib/utils";
6
+
7
+ const buttonVariants = cva(
8
+ "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
9
+ {
10
+ variants: {
11
+ variant: {
12
+ default: "bg-primary text-primary-foreground hover:bg-primary/90",
13
+ destructive:
14
+ "bg-destructive text-white hover:bg-destructive/90",
15
+ outline:
16
+ "border border-input bg-background hover:bg-accent hover:text-accent-foreground",
17
+ secondary:
18
+ "bg-secondary text-secondary-foreground hover:bg-secondary/80",
19
+ ghost: "hover:bg-accent hover:text-accent-foreground",
20
+ link: "text-primary underline-offset-4 hover:underline",
21
+ },
22
+ size: {
23
+ default: "h-9 px-4 py-2",
24
+ sm: "h-8 rounded-md px-3 text-xs",
25
+ lg: "h-10 rounded-md px-8",
26
+ icon: "h-9 w-9",
27
+ },
28
+ },
29
+ defaultVariants: {
30
+ variant: "default",
31
+ size: "default",
32
+ },
33
+ },
34
+ );
35
+
36
+ export interface ButtonProps
37
+ extends React.ButtonHTMLAttributes<HTMLButtonElement>,
38
+ VariantProps<typeof buttonVariants> {
39
+ asChild?: boolean;
40
+ }
41
+
42
+ const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
43
+ ({ className, variant, size, asChild = false, ...props }, ref) => {
44
+ const Comp = asChild ? Slot : "button";
45
+ return (
46
+ <Comp
47
+ className={cn(buttonVariants({ variant, size, className }))}
48
+ ref={ref}
49
+ {...props}
50
+ />
51
+ );
52
+ },
53
+ );
54
+ Button.displayName = "Button";
55
+
56
+ export { Button, buttonVariants };
@@ -0,0 +1,90 @@
1
+ import * as React from "react";
2
+
3
+ import { cn } from "@/lib/utils";
4
+
5
+ function Card({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) {
6
+ return (
7
+ <div
8
+ data-slot="card"
9
+ className={cn(
10
+ "rounded-xl border bg-card text-card-foreground shadow-sm",
11
+ className,
12
+ )}
13
+ {...props}
14
+ />
15
+ );
16
+ }
17
+
18
+ function CardHeader({
19
+ className,
20
+ ...props
21
+ }: React.HTMLAttributes<HTMLDivElement>) {
22
+ return (
23
+ <div
24
+ data-slot="card-header"
25
+ className={cn("flex flex-col space-y-1.5 p-6", className)}
26
+ {...props}
27
+ />
28
+ );
29
+ }
30
+
31
+ function CardTitle({
32
+ className,
33
+ ...props
34
+ }: React.HTMLAttributes<HTMLDivElement>) {
35
+ return (
36
+ <div
37
+ data-slot="card-title"
38
+ className={cn("font-semibold leading-none tracking-tight", className)}
39
+ {...props}
40
+ />
41
+ );
42
+ }
43
+
44
+ function CardDescription({
45
+ className,
46
+ ...props
47
+ }: React.HTMLAttributes<HTMLDivElement>) {
48
+ return (
49
+ <div
50
+ data-slot="card-description"
51
+ className={cn("text-sm text-muted-foreground", className)}
52
+ {...props}
53
+ />
54
+ );
55
+ }
56
+
57
+ function CardContent({
58
+ className,
59
+ ...props
60
+ }: React.HTMLAttributes<HTMLDivElement>) {
61
+ return (
62
+ <div
63
+ data-slot="card-content"
64
+ className={cn("p-6 pt-0", className)}
65
+ {...props}
66
+ />
67
+ );
68
+ }
69
+
70
+ function CardFooter({
71
+ className,
72
+ ...props
73
+ }: React.HTMLAttributes<HTMLDivElement>) {
74
+ return (
75
+ <div
76
+ data-slot="card-footer"
77
+ className={cn("flex items-center p-6 pt-0", className)}
78
+ {...props}
79
+ />
80
+ );
81
+ }
82
+
83
+ export {
84
+ Card,
85
+ CardHeader,
86
+ CardFooter,
87
+ CardTitle,
88
+ CardDescription,
89
+ CardContent,
90
+ };
@@ -0,0 +1,20 @@
1
+ {
2
+ "$schema": "https://ui.shadcn.com/schema.json",
3
+ "style": "new-york",
4
+ "rsc": false,
5
+ "tsx": true,
6
+ "tailwind": {
7
+ "config": "",
8
+ "css": "app/globals.css",
9
+ "baseColor": "zinc",
10
+ "cssVariables": true
11
+ },
12
+ "aliases": {
13
+ "components": "@/components",
14
+ "utils": "@/lib/utils",
15
+ "ui": "@/components/ui",
16
+ "lib": "@/lib",
17
+ "hooks": "@/hooks"
18
+ },
19
+ "iconLibrary": "lucide"
20
+ }