@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,249 @@
1
+ import React, { Suspense, use } from "react";
2
+ import { Link, type Metadata, type PageProps, type ServerData } from "@pylonsync/react";
3
+ import {
4
+ WRAP,
5
+ Eyebrow,
6
+ Divider,
7
+ SectionHead,
8
+ ImagePlaceholder,
9
+ ProjectCard,
10
+ initials,
11
+ } from "@/components/marketing";
12
+ import { LiveSlots, ContactForm } from "./contact-form";
13
+ import { SeedProjects } from "./seeder";
14
+ import { siteConfig } from "@/lib/site.config";
15
+ import { slugify, viewFromRow, type ProjectRow, type ProjectView } from "@/lib/agency";
16
+
17
+ export const metadata: Metadata = {
18
+ title: siteConfig.seo.title,
19
+ description: siteConfig.seo.description,
20
+ openGraph: { title: siteConfig.seo.title, description: siteConfig.seo.description, type: "website" },
21
+ };
22
+
23
+ // The homepage "Selected work" grid reads the live Project portfolio on the
24
+ // server (the `selected` + `published` ones, ordered), so curating it in the
25
+ // dashboard re-curates the homepage. Before the portfolio is seeded, it falls
26
+ // back to the config case studies so the section is never empty on first paint.
27
+ function selectedFromConfig(): ProjectView[] {
28
+ return siteConfig.work.items
29
+ .filter((c) => c.selected)
30
+ .map((c) => ({
31
+ slug: c.slug || slugify(c.title),
32
+ title: c.title,
33
+ client: c.client,
34
+ summary: c.summary,
35
+ year: c.year ?? null,
36
+ tags: c.tags,
37
+ }));
38
+ }
39
+
40
+ function SelectedWork({ serverData }: { serverData: ServerData }) {
41
+ const rows = use(serverData.list<ProjectRow>("Project"));
42
+ const fromDb = rows
43
+ .filter((p) => p.selected && p.published)
44
+ .sort((a, b) => a.order - b.order || (a.createdAt < b.createdAt ? -1 : 1))
45
+ .map(viewFromRow);
46
+ const projects = fromDb.length > 0 ? fromDb : selectedFromConfig();
47
+
48
+ return (
49
+ <div className="mt-10 grid gap-6 sm:grid-cols-2">
50
+ {projects.map((p) => (
51
+ <ProjectCard key={p.slug} p={p} />
52
+ ))}
53
+ </div>
54
+ );
55
+ }
56
+
57
+ // `app/page.tsx` → `/`. Server-rendered studio site. Hero, services, work,
58
+ // process, team, and testimonials are static server HTML (SEO + first paint);
59
+ // the live "slots open" pill and the contact form (#contact) are client islands
60
+ // driven by the public Capacity row. The "Selected work" grid reads the Project
61
+ // portfolio server-side. All other copy comes from siteConfig.
62
+ export default function LandingPage({ serverData }: PageProps) {
63
+ const { hero, logos, services, work, process, team, testimonials, contact } = siteConfig;
64
+
65
+ return (
66
+ <div className="bg-white text-zinc-900">
67
+ {/* ============================== HERO ============================== */}
68
+ <section className={`${WRAP} pt-16 pb-14 sm:pt-20`}>
69
+ <div className="grid items-center gap-10 lg:grid-cols-2 lg:gap-14">
70
+ <div>
71
+ <p className="font-mono text-[11px] uppercase tracking-[0.16em] text-brand">{hero.tagline}</p>
72
+ <h1 className="mt-4 text-balance text-[2.5rem] font-semibold leading-[1.04] tracking-[-0.02em] sm:text-[3.25rem]">
73
+ {hero.headline}
74
+ </h1>
75
+ <p className="mt-5 max-w-xl text-[17px] leading-relaxed text-zinc-500">{hero.subcopy}</p>
76
+ <div className="mt-7 flex flex-wrap items-center gap-4">
77
+ <a href="#contact" className="inline-flex items-center rounded-full bg-brand px-5 py-2.5 text-sm font-medium text-white transition-opacity hover:opacity-90">
78
+ {hero.ctaLabel}
79
+ </a>
80
+ <a href="#work" className="text-sm font-medium text-zinc-700 hover:text-zinc-900">
81
+ {hero.secondaryCtaLabel} →
82
+ </a>
83
+ </div>
84
+ <div className="mt-8">
85
+ <LiveSlots />
86
+ </div>
87
+ </div>
88
+
89
+ {/* Hero photo — replace the placeholder with a real shot (the studio,
90
+ the team at work, a flagship project). */}
91
+ <div className="relative mx-auto w-full max-w-sm lg:max-w-none">
92
+ <ImagePlaceholder
93
+ shape="portrait"
94
+ title="A photo of your studio or work"
95
+ hint="Swap for an <img> in app/page.tsx"
96
+ />
97
+ </div>
98
+ </div>
99
+ </section>
100
+
101
+ {/* ============================== LOGOS ============================= */}
102
+ <section className={`${WRAP} pb-6`}>
103
+ <p className="text-center font-mono text-[11px] uppercase tracking-[0.16em] text-zinc-400">
104
+ {logos.eyebrow}
105
+ </p>
106
+ <div className="mt-5 flex flex-wrap items-center justify-center gap-x-10 gap-y-4">
107
+ {logos.names.map((n) => (
108
+ <span key={n} className="text-[15px] font-semibold tracking-tight text-zinc-300">
109
+ {n}
110
+ </span>
111
+ ))}
112
+ </div>
113
+ </section>
114
+
115
+ {/* ============================ SERVICES =========================== */}
116
+ <Divider />
117
+ <section id="services" className={`${WRAP} py-16`}>
118
+ <SectionHead eyebrow={services.eyebrow} title={services.headline} />
119
+ <div className="mt-10 grid gap-x-8 gap-y-10 sm:grid-cols-2">
120
+ {services.items.map((s) => (
121
+ <div key={s.title}>
122
+ {s.icon ? (
123
+ <span className="flex size-9 items-center justify-center rounded-lg bg-brand-soft text-brand">
124
+ {s.icon}
125
+ </span>
126
+ ) : null}
127
+ <h3 className="mt-4 text-[16px] font-semibold text-zinc-900">{s.title}</h3>
128
+ <p className="mt-2 max-w-md text-[14px] leading-relaxed text-zinc-500">{s.body}</p>
129
+ </div>
130
+ ))}
131
+ </div>
132
+ </section>
133
+
134
+ {/* ============================== WORK ============================= */}
135
+ <Divider />
136
+ <section id="work" className={`${WRAP} py-16`}>
137
+ <div className="flex items-end justify-between gap-4">
138
+ <SectionHead eyebrow={work.eyebrow} title={work.headline} />
139
+ <Link
140
+ href="/work"
141
+ className="hidden shrink-0 text-[13.5px] font-medium text-zinc-600 transition-colors hover:text-zinc-900 sm:inline-flex"
142
+ >
143
+ All work →
144
+ </Link>
145
+ </div>
146
+ <Suspense
147
+ fallback={
148
+ <div className="mt-10 grid gap-6 sm:grid-cols-2">
149
+ {[0, 1].map((i) => (
150
+ <div key={i}>
151
+ <div className="aspect-[4/3] animate-pulse rounded-2xl bg-zinc-100" />
152
+ <div className="mt-4 h-4 w-1/3 animate-pulse rounded bg-zinc-100" />
153
+ <div className="mt-2 h-3 w-3/4 animate-pulse rounded bg-zinc-100" />
154
+ </div>
155
+ ))}
156
+ </div>
157
+ }
158
+ >
159
+ <SelectedWork serverData={serverData} />
160
+ </Suspense>
161
+ <div className="mt-8 sm:hidden">
162
+ <Link href="/work" className="text-[14px] font-medium text-brand">
163
+ See all work →
164
+ </Link>
165
+ </div>
166
+ </section>
167
+
168
+ {/* ============================ PROCESS ============================ */}
169
+ <Divider />
170
+ <section className={`${WRAP} py-16`}>
171
+ <SectionHead eyebrow={process.eyebrow} title={process.headline} />
172
+ <ol className="mt-10 grid gap-8 sm:grid-cols-2 lg:grid-cols-4">
173
+ {process.steps.map((step, i) => (
174
+ <li key={step.title}>
175
+ <span className="flex size-8 items-center justify-center rounded-full bg-zinc-900 font-mono text-[12px] font-semibold text-white">
176
+ {i + 1}
177
+ </span>
178
+ <h3 className="mt-4 text-[15px] font-semibold text-zinc-900">{step.title}</h3>
179
+ <p className="mt-2 text-[14px] leading-relaxed text-zinc-500">{step.body}</p>
180
+ </li>
181
+ ))}
182
+ </ol>
183
+ </section>
184
+
185
+ {/* ============================== TEAM ============================= */}
186
+ <Divider />
187
+ <section className={`${WRAP} py-16`}>
188
+ <SectionHead eyebrow={team.eyebrow} title={team.headline} />
189
+ <div className="mt-10 grid gap-8 sm:grid-cols-3">
190
+ {team.members.map((m) => (
191
+ <div key={m.name}>
192
+ {/* Team headshot — drop in a real photo. */}
193
+ <ImagePlaceholder shape="square" title="Headshot" hint="Replace per team member" className="max-w-[200px]" />
194
+ <h3 className="mt-4 text-[15px] font-semibold text-zinc-900">{m.name}</h3>
195
+ <p className="text-[13.5px] text-zinc-500">{m.role}</p>
196
+ </div>
197
+ ))}
198
+ </div>
199
+ </section>
200
+
201
+ {/* ========================== TESTIMONIALS ========================= */}
202
+ {testimonials ? (
203
+ <>
204
+ <Divider />
205
+ <section className={`${WRAP} py-16`}>
206
+ <SectionHead eyebrow={testimonials.eyebrow} title={testimonials.headline} />
207
+ <div className="mt-10 grid gap-6 sm:grid-cols-3">
208
+ {testimonials.items.map((t) => (
209
+ <figure key={t.name} className="flex flex-col rounded-2xl border border-zinc-200 bg-paper p-6">
210
+ <blockquote className="flex-1 text-[14px] leading-relaxed text-zinc-700">“{t.quote}”</blockquote>
211
+ <figcaption className="mt-5 flex items-center gap-3">
212
+ <span className="flex size-9 items-center justify-center rounded-full bg-zinc-200 text-[11px] font-semibold text-zinc-500">
213
+ {initials(t.name)}
214
+ </span>
215
+ <span className="text-[13px] leading-tight">
216
+ <span className="block font-medium text-zinc-900">{t.name}</span>
217
+ <span className="text-zinc-500">{t.role}</span>
218
+ </span>
219
+ </figcaption>
220
+ </figure>
221
+ ))}
222
+ </div>
223
+ </section>
224
+ </>
225
+ ) : null}
226
+
227
+ {/* ============================= CONTACT =========================== */}
228
+ <Divider />
229
+ <section id="contact" className={`${WRAP} py-16`}>
230
+ <div className="grid gap-10 lg:grid-cols-[0.9fr_1.1fr]">
231
+ <div>
232
+ <Eyebrow>{contact.eyebrow}</Eyebrow>
233
+ <h2 className="mt-4 text-balance text-2xl font-semibold leading-[1.15] tracking-[-0.02em] sm:text-3xl">
234
+ {contact.headline}
235
+ </h2>
236
+ <p className="mt-4 max-w-md text-[15px] leading-relaxed text-zinc-500">{contact.subcopy}</p>
237
+ <div className="mt-6">
238
+ <LiveSlots />
239
+ </div>
240
+ </div>
241
+ <ContactForm />
242
+ </div>
243
+ </section>
244
+
245
+ {/* Seeds the public portfolio on first visit (idempotent, zero UI). */}
246
+ <SeedProjects />
247
+ </div>
248
+ );
249
+ }
@@ -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,26 @@
1
+ "use client";
2
+
3
+ import React, { useEffect } from "react";
4
+ import { callFn } from "@pylonsync/react";
5
+ import { EnsureGuest } from "@pylonsync/client";
6
+
7
+ // A zero-UI client island that seeds the public Project portfolio on first
8
+ // visit (idempotent server-side — a no-op once any project exists). Drop it on
9
+ // any public page so /work and the case-study pages aren't empty even if the
10
+ // visitor never hit the homepage. Wrapped in <EnsureGuest> so a session exists
11
+ // for the call; seedProjects is a public mutation, so an anonymous guest can run
12
+ // it (it only ever writes the config's marketing copy — no PII).
13
+ export function SeedProjects() {
14
+ return (
15
+ <EnsureGuest fallback={null}>
16
+ <Seed />
17
+ </EnsureGuest>
18
+ );
19
+ }
20
+
21
+ function Seed() {
22
+ useEffect(() => {
23
+ void callFn("seedProjects", {});
24
+ }, []);
25
+ return null;
26
+ }
@@ -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 studio site 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,182 @@
1
+ import React, { Suspense, use } from "react";
2
+ import {
3
+ Link,
4
+ type GenerateMetadata,
5
+ type Metadata,
6
+ type PageProps,
7
+ type ServerData,
8
+ type SsrResponse,
9
+ } from "@pylonsync/react";
10
+ import { ImagePlaceholder } from "@/components/marketing";
11
+ import { SeedProjects } from "../../seeder";
12
+ import { siteConfig } from "@/lib/site.config";
13
+ import { slugify, viewFromRow, type ProjectRow, type ProjectView } from "@/lib/agency";
14
+
15
+ // Resolve a case study from the URL slug. Prefer the live Project row; if the
16
+ // row exists but is a draft (`published === false`) it's NOT public → treat as
17
+ // missing. Only when there's NO row at all (the portfolio hasn't been seeded
18
+ // yet) do we fall back to the matching config case study, so deep links work on
19
+ // a fresh install.
20
+ async function resolveProject(
21
+ serverData: ServerData,
22
+ slug: string,
23
+ ): Promise<ProjectView | null> {
24
+ const row = await serverData.lookup<ProjectRow>("Project", "slug", slug);
25
+ if (row) return row.published ? viewFromRow(row) : null;
26
+
27
+ const cfg = siteConfig.work.items.find((c) => (c.slug || slugify(c.title)) === slug);
28
+ if (!cfg) return null;
29
+ return {
30
+ slug: cfg.slug || slugify(cfg.title),
31
+ title: cfg.title,
32
+ client: cfg.client,
33
+ summary: cfg.summary,
34
+ year: cfg.year ?? null,
35
+ tags: cfg.tags,
36
+ challenge: cfg.challenge ?? null,
37
+ approach: cfg.approach ?? null,
38
+ outcome: cfg.outcome ?? null,
39
+ liveUrl: cfg.liveUrl ?? null,
40
+ };
41
+ }
42
+
43
+ export const generateMetadata: GenerateMetadata = async ({
44
+ params,
45
+ serverData,
46
+ }): Promise<Metadata> => {
47
+ const p = await resolveProject(serverData, params.slug);
48
+ if (!p) return { title: `Case study not found — ${siteConfig.brand.name}`, robots: "noindex" };
49
+ return {
50
+ title: `${p.title} — ${siteConfig.brand.name}`,
51
+ description: p.summary,
52
+ openGraph: { title: `${p.title} — ${siteConfig.brand.name}`, description: p.summary, type: "article" },
53
+ };
54
+ };
55
+
56
+ const WRAP_NARROW = "mx-auto w-full max-w-3xl px-6";
57
+
58
+ function CaseStudy({
59
+ serverData,
60
+ response,
61
+ slug,
62
+ }: {
63
+ serverData: ServerData;
64
+ response: SsrResponse;
65
+ slug: string;
66
+ }) {
67
+ const p = use(resolveProject(serverData, slug));
68
+
69
+ if (!p) {
70
+ response.setStatus(404);
71
+ return (
72
+ <div className={`${WRAP_NARROW} py-24 text-center`}>
73
+ <p className="text-[15px] font-medium text-zinc-900">That case study doesn&apos;t exist.</p>
74
+ <Link href="/work" className="mt-2 inline-block text-[14px] font-medium text-brand">
75
+ ← Back to all work
76
+ </Link>
77
+ </div>
78
+ );
79
+ }
80
+
81
+ return (
82
+ <article className={`${WRAP_NARROW} py-14`}>
83
+ <Link href="/work" className="text-[13.5px] font-medium text-zinc-500 transition-colors hover:text-zinc-900">
84
+ ← All work
85
+ </Link>
86
+
87
+ <header className="mt-6">
88
+ <p className="font-mono text-[11px] uppercase tracking-[0.16em] text-brand">
89
+ {p.client}
90
+ {p.year ? ` · ${p.year}` : ""}
91
+ </p>
92
+ <h1 className="mt-3 text-balance text-[2rem] font-semibold leading-[1.08] tracking-[-0.02em] sm:text-[2.5rem]">
93
+ {p.title}
94
+ </h1>
95
+ <p className="mt-4 max-w-2xl text-[17px] leading-relaxed text-zinc-500">{p.summary}</p>
96
+ {p.tags.length > 0 ? (
97
+ <div className="mt-5 flex flex-wrap gap-1.5">
98
+ {p.tags.map((t) => (
99
+ <span key={t} className="rounded-full bg-zinc-100 px-2.5 py-0.5 text-[11px] font-medium text-zinc-600">
100
+ {t}
101
+ </span>
102
+ ))}
103
+ </div>
104
+ ) : null}
105
+ {p.liveUrl ? (
106
+ <a
107
+ href={p.liveUrl}
108
+ target="_blank"
109
+ rel="noopener noreferrer"
110
+ className="mt-6 inline-flex items-center rounded-full border border-zinc-300 px-4 py-2 text-[13.5px] font-medium text-zinc-700 transition-colors hover:border-zinc-400 hover:text-zinc-900"
111
+ >
112
+ Visit live site ↗
113
+ </a>
114
+ ) : null}
115
+ </header>
116
+
117
+ {/* Hero shot — drop in a real project image. */}
118
+ <div className="mt-10">
119
+ <ImagePlaceholder shape="landscape" title={`${p.title} — hero shot`} hint="Swap for an <img> in app/work/[slug]/page.tsx" />
120
+ </div>
121
+
122
+ <div className="mt-12 space-y-10">
123
+ <CaseSection label="The challenge" body={p.challenge} />
124
+ <CaseSection label="Our approach" body={p.approach} />
125
+ <CaseSection label="The outcome" body={p.outcome} />
126
+ {!p.challenge && !p.approach && !p.outcome ? (
127
+ <p className="text-[15px] leading-relaxed text-zinc-500">
128
+ A full write-up is on the way. In the meantime, {p.summary.toLowerCase()}
129
+ </p>
130
+ ) : null}
131
+ </div>
132
+
133
+ {/* Contact CTA */}
134
+ <div className="mt-14 rounded-2xl border border-zinc-200 bg-paper p-8 text-center">
135
+ <h2 className="text-[18px] font-semibold tracking-tight text-zinc-900">Have something like this in mind?</h2>
136
+ <p className="mx-auto mt-2 max-w-md text-[14px] leading-relaxed text-zinc-500">
137
+ We take on a few projects at a time. Tell us what you&apos;re building.
138
+ </p>
139
+ <Link
140
+ href="/#contact"
141
+ className="mt-5 inline-flex items-center rounded-full bg-brand px-5 py-2.5 text-[14px] font-medium text-white transition-opacity hover:opacity-90"
142
+ >
143
+ Start a project
144
+ </Link>
145
+ </div>
146
+
147
+ <SeedProjects />
148
+ </article>
149
+ );
150
+ }
151
+
152
+ function CaseSection({ label, body }: { label: string; body?: string | null }) {
153
+ if (!body) return null;
154
+ return (
155
+ <section>
156
+ <h2 className="font-mono text-[11px] uppercase tracking-[0.16em] text-zinc-400">{label}</h2>
157
+ <p className="mt-3 whitespace-pre-wrap text-[16px] leading-relaxed text-zinc-700">{body}</p>
158
+ </section>
159
+ );
160
+ }
161
+
162
+ // `app/work/[slug]/page.tsx` → `/work/:slug`. The case-study detail. Suspends on
163
+ // the project lookup; renders a 404 (with a real status) when the slug is
164
+ // unknown or the project is a draft.
165
+ export default function CaseStudyPage({ params, serverData, response }: PageProps) {
166
+ return (
167
+ <div className="bg-white text-zinc-900">
168
+ <Suspense
169
+ fallback={
170
+ <div className={`${WRAP_NARROW} py-14`}>
171
+ <div className="h-4 w-20 animate-pulse rounded bg-zinc-100" />
172
+ <div className="mt-6 h-10 w-2/3 animate-pulse rounded bg-zinc-100" />
173
+ <div className="mt-4 h-4 w-full animate-pulse rounded bg-zinc-100" />
174
+ <div className="mt-10 aspect-[4/3] animate-pulse rounded-2xl bg-zinc-100" />
175
+ </div>
176
+ }
177
+ >
178
+ <CaseStudy serverData={serverData} response={response} slug={params.slug} />
179
+ </Suspense>
180
+ </div>
181
+ );
182
+ }
@@ -0,0 +1,83 @@
1
+ import React, { Suspense, use } from "react";
2
+ import { Link, type Metadata, type PageProps, type ServerData } from "@pylonsync/react";
3
+ import { WRAP, SectionHead, ProjectCard } from "@/components/marketing";
4
+ import { SeedProjects } from "../seeder";
5
+ import { siteConfig } from "@/lib/site.config";
6
+ import { slugify, viewFromRow, type ProjectRow, type ProjectView } from "@/lib/agency";
7
+
8
+ export const metadata: Metadata = {
9
+ title: `Work — ${siteConfig.brand.name}`,
10
+ description: `Case studies from ${siteConfig.brand.name}: ${siteConfig.work.headline}`,
11
+ openGraph: { title: `Work — ${siteConfig.brand.name}`, type: "website" },
12
+ };
13
+
14
+ // `app/work/page.tsx` → `/work`. The full portfolio: every PUBLISHED project,
15
+ // server-rendered for SEO, each linking to its case study. Reads the Project
16
+ // entity via serverData; falls back to the config case studies before the
17
+ // portfolio is seeded so the page is never empty.
18
+ function allFromConfig(): ProjectView[] {
19
+ return siteConfig.work.items.map((c) => ({
20
+ slug: c.slug || slugify(c.title),
21
+ title: c.title,
22
+ client: c.client,
23
+ summary: c.summary,
24
+ year: c.year ?? null,
25
+ tags: c.tags,
26
+ }));
27
+ }
28
+
29
+ function WorkGrid({ serverData }: { serverData: ServerData }) {
30
+ const rows = use(serverData.list<ProjectRow>("Project"));
31
+ const fromDb = rows
32
+ .filter((p) => p.published)
33
+ .sort((a, b) => a.order - b.order || (a.createdAt < b.createdAt ? -1 : 1))
34
+ .map(viewFromRow);
35
+ const projects = fromDb.length > 0 ? fromDb : allFromConfig();
36
+
37
+ if (projects.length === 0) {
38
+ return <p className="mt-10 text-[15px] text-zinc-500">No published work yet.</p>;
39
+ }
40
+ return (
41
+ <div className="mt-10 grid gap-x-6 gap-y-10 sm:grid-cols-2">
42
+ {projects.map((p) => (
43
+ <ProjectCard key={p.slug} p={p} />
44
+ ))}
45
+ </div>
46
+ );
47
+ }
48
+
49
+ export default function WorkIndexPage({ serverData }: PageProps) {
50
+ return (
51
+ <div className="bg-white text-zinc-900">
52
+ <section className={`${WRAP} py-16`}>
53
+ <SectionHead
54
+ eyebrow="Work"
55
+ title={siteConfig.work.headline}
56
+ body="A fuller look at what we've shipped — each one a short case study."
57
+ />
58
+ <Suspense
59
+ fallback={
60
+ <div className="mt-10 grid gap-x-6 gap-y-10 sm:grid-cols-2">
61
+ {[0, 1, 2, 3].map((i) => (
62
+ <div key={i}>
63
+ <div className="aspect-[4/3] animate-pulse rounded-2xl bg-zinc-100" />
64
+ <div className="mt-4 h-4 w-1/3 animate-pulse rounded bg-zinc-100" />
65
+ </div>
66
+ ))}
67
+ </div>
68
+ }
69
+ >
70
+ <WorkGrid serverData={serverData} />
71
+ </Suspense>
72
+
73
+ <div className="mt-12">
74
+ <Link href="/#contact" className="text-[14px] font-medium text-brand">
75
+ Have a project in mind? Start one →
76
+ </Link>
77
+ </div>
78
+ </section>
79
+
80
+ <SeedProjects />
81
+ </div>
82
+ );
83
+ }