@pylonsync/create-pylon 0.11.1 → 0.11.3

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 (142) hide show
  1. package/bin/create-pylon.js +1 -1
  2. package/package.json +1 -1
  3. package/templates/_root/AGENTS.md +1 -1
  4. package/templates/agency/AGENTS.md +1 -1
  5. package/templates/agency/app/(marketing)/contact-form.tsx +22 -32
  6. package/templates/agency/app/(marketing)/layout.tsx +36 -64
  7. package/templates/agency/app/(marketing)/not-found.tsx +6 -8
  8. package/templates/agency/app/(marketing)/page.tsx +93 -144
  9. package/templates/agency/app/(marketing)/work/[slug]/page.tsx +69 -71
  10. package/templates/agency/app/(marketing)/work/page.tsx +20 -17
  11. package/templates/agency/app/globals.css +15 -6
  12. package/templates/agency/app.ts +10 -0
  13. package/templates/agency/components/marketing.tsx +40 -104
  14. package/templates/agency/lib/site.config.ts +34 -52
  15. package/templates/agency/public/images/hero.jpg +0 -0
  16. package/templates/agency/public/images/team/claire-donovan.jpg +0 -0
  17. package/templates/agency/public/images/team/dana-okafor.jpg +0 -0
  18. package/templates/agency/public/images/team/marcus-lee.jpg +0 -0
  19. package/templates/agency/public/images/work/atlas-health.jpg +0 -0
  20. package/templates/agency/public/images/work/cohort.jpg +0 -0
  21. package/templates/agency/public/images/work/ledger.jpg +0 -0
  22. package/templates/agency/public/images/work/vela.jpg +0 -0
  23. package/templates/ai-chat/AGENTS.md +1 -1
  24. package/templates/ai-studio/AGENTS.md +1 -1
  25. package/templates/barebones/AGENTS.md +1 -1
  26. package/templates/chat/AGENTS.md +1 -1
  27. package/templates/consumer/AGENTS.md +1 -1
  28. package/templates/consumer/README.md +39 -23
  29. package/templates/consumer/app/(app)/explore/page.tsx +13 -0
  30. package/templates/consumer/app/(app)/layout.tsx +17 -0
  31. package/templates/consumer/app/(app)/p/[id]/page.tsx +13 -0
  32. package/templates/consumer/app/(app)/page.tsx +15 -0
  33. package/templates/consumer/app/(app)/u/[username]/page.tsx +14 -0
  34. package/templates/consumer/app/error.tsx +1 -1
  35. package/templates/consumer/app/globals.css +25 -0
  36. package/templates/consumer/app/layout.tsx +3 -10
  37. package/templates/consumer/app/llms.ts +10 -10
  38. package/templates/consumer/app/not-found.tsx +9 -22
  39. package/templates/consumer/app/robots.ts +3 -4
  40. package/templates/consumer/app/sitemap.ts +7 -21
  41. package/templates/consumer/app.ts +152 -33
  42. package/templates/consumer/components/social/app-chrome.tsx +109 -0
  43. package/templates/consumer/components/social/avatar.tsx +50 -0
  44. package/templates/consumer/components/social/comment-form.tsx +51 -0
  45. package/templates/consumer/components/social/create-post.tsx +155 -0
  46. package/templates/consumer/components/social/edit-profile.tsx +103 -0
  47. package/templates/consumer/components/social/explore-screen.tsx +23 -0
  48. package/templates/consumer/components/social/home-screen.tsx +161 -0
  49. package/templates/consumer/components/social/post-card.tsx +77 -0
  50. package/templates/consumer/components/social/post-grid.tsx +46 -0
  51. package/templates/consumer/components/social/post-parts.tsx +146 -0
  52. package/templates/consumer/components/social/post-screen.tsx +173 -0
  53. package/templates/consumer/components/social/profile-screen.tsx +146 -0
  54. package/templates/consumer/components/social/rich-text.tsx +22 -0
  55. package/templates/consumer/components/social/session.tsx +43 -0
  56. package/templates/consumer/components/social/use-social.ts +86 -0
  57. package/templates/consumer/functions/addComment.ts +27 -0
  58. package/templates/consumer/functions/createPost.ts +39 -0
  59. package/templates/consumer/functions/ensureProfile.ts +39 -0
  60. package/templates/consumer/functions/seedFeed.ts +38 -0
  61. package/templates/consumer/functions/updateProfile.ts +61 -0
  62. package/templates/consumer/lib/seed.ts +192 -0
  63. package/templates/consumer/lib/site.ts +6 -0
  64. package/templates/consumer/lib/social.ts +245 -0
  65. package/templates/consumer/tests/social.test.ts +157 -0
  66. package/templates/creator/AGENTS.md +1 -1
  67. package/templates/creator/app/(marketing)/layout.tsx +35 -69
  68. package/templates/creator/app/(marketing)/newsletter-signup.tsx +15 -26
  69. package/templates/creator/app/(marketing)/not-found.tsx +8 -6
  70. package/templates/creator/app/(marketing)/page.tsx +90 -106
  71. package/templates/creator/app/globals.css +14 -0
  72. package/templates/creator/app/layout.tsx +1 -0
  73. package/templates/creator/app.ts +10 -0
  74. package/templates/creator/components/marketing.tsx +28 -89
  75. package/templates/creator/lib/site.config.ts +51 -29
  76. package/templates/creator/public/images/headshot.jpg +0 -0
  77. package/templates/crm/AGENTS.md +1 -1
  78. package/templates/crm/lib/seed.ts +153 -21
  79. package/templates/directory/AGENTS.md +1 -1
  80. package/templates/directory/app/(marketing)/directory-browse.tsx +99 -137
  81. package/templates/directory/app/(marketing)/layout.tsx +37 -88
  82. package/templates/directory/app/(marketing)/not-found.tsx +11 -10
  83. package/templates/directory/app/(marketing)/page.tsx +12 -33
  84. package/templates/directory/app/(marketing)/submit/page.tsx +9 -10
  85. package/templates/directory/app/(marketing)/submit-form.tsx +26 -26
  86. package/templates/directory/app/globals.css +15 -4
  87. package/templates/directory/app.ts +10 -0
  88. package/templates/directory/components/marketing.tsx +27 -91
  89. package/templates/directory/lib/site.config.ts +13 -24
  90. package/templates/helpdesk/AGENTS.md +1 -1
  91. package/templates/helpdesk/lib/seed.ts +188 -104
  92. package/templates/inventory/AGENTS.md +1 -1
  93. package/templates/inventory/lib/seed.ts +175 -44
  94. package/templates/invoices/AGENTS.md +1 -1
  95. package/templates/invoices/lib/seed.ts +74 -89
  96. package/templates/local-service/AGENTS.md +1 -1
  97. package/templates/local-service/app/(marketing)/booking-widget.tsx +31 -31
  98. package/templates/local-service/app/(marketing)/layout.tsx +25 -37
  99. package/templates/local-service/app/(marketing)/not-found.tsx +4 -4
  100. package/templates/local-service/app/(marketing)/page.tsx +145 -139
  101. package/templates/local-service/app/globals.css +11 -0
  102. package/templates/local-service/app/layout.tsx +2 -0
  103. package/templates/local-service/app.ts +9 -0
  104. package/templates/local-service/components/marketing.tsx +14 -39
  105. package/templates/local-service/lib/site.config.ts +16 -23
  106. package/templates/local-service/public/images/hero.jpg +0 -0
  107. package/templates/marketplace/AGENTS.md +1 -1
  108. package/templates/marketplace/client/SellForm.tsx +2 -0
  109. package/templates/projects/AGENTS.md +1 -1
  110. package/templates/projects/lib/seed.ts +103 -32
  111. package/templates/restaurant/AGENTS.md +1 -1
  112. package/templates/restaurant/app/(marketing)/layout.tsx +26 -48
  113. package/templates/restaurant/app/(marketing)/page.tsx +146 -123
  114. package/templates/restaurant/app/globals.css +11 -0
  115. package/templates/restaurant/app/layout.tsx +1 -0
  116. package/templates/restaurant/app.ts +10 -0
  117. package/templates/restaurant/components/marketing.tsx +10 -0
  118. package/templates/restaurant/lib/site.config.ts +16 -14
  119. package/templates/restaurant/public/images/bar.jpg +0 -0
  120. package/templates/restaurant/public/images/branzino.jpg +0 -0
  121. package/templates/restaurant/public/images/hearth.jpg +0 -0
  122. package/templates/restaurant/public/images/hero.jpg +0 -0
  123. package/templates/saas/AGENTS.md +1 -1
  124. package/templates/shop/AGENTS.md +1 -1
  125. package/templates/shop/public/images/products/cedar-smoke.jpg +0 -0
  126. package/templates/shop/public/images/products/linen.jpg +0 -0
  127. package/templates/todo/AGENTS.md +1 -1
  128. package/templates/waitlist/AGENTS.md +1 -1
  129. package/templates/waitlist/app/(marketing)/layout.tsx +39 -85
  130. package/templates/waitlist/app/(marketing)/not-found.tsx +8 -9
  131. package/templates/waitlist/app/(marketing)/page.tsx +51 -86
  132. package/templates/waitlist/app/(marketing)/waitlist-hero.tsx +43 -83
  133. package/templates/waitlist/app/globals.css +23 -0
  134. package/templates/waitlist/app/layout.tsx +1 -0
  135. package/templates/waitlist/app.ts +18 -0
  136. package/templates/waitlist/components/marketing.tsx +67 -81
  137. package/templates/waitlist/lib/site.config.ts +52 -59
  138. package/templates/consumer/app/feed-client.tsx +0 -159
  139. package/templates/consumer/app/page.tsx +0 -27
  140. package/templates/consumer/functions/_keep.ts +0 -13
  141. package/templates/consumer/functions/seedPosts.ts +0 -56
  142. package/templates/consumer/tests/example.test.ts +0 -12
@@ -147,6 +147,24 @@ const manifest = buildManifest({
147
147
  display: "swap",
148
148
  preload: true,
149
149
  }),
150
+ // Headlines and the product mock's window title.
151
+ font({
152
+ family: "Space Grotesk",
153
+ variable: "--font-display",
154
+ weights: ["500", "700"],
155
+ subsets: ["latin"],
156
+ display: "swap",
157
+ preload: true,
158
+ }),
159
+ // Small details: the launch date, the live count, the form hint.
160
+ font({
161
+ family: "JetBrains Mono",
162
+ variable: "--font-mono-ui",
163
+ weights: ["400", "500"],
164
+ subsets: ["latin"],
165
+ display: "swap",
166
+ preload: false,
167
+ }),
150
168
  ],
151
169
  routes: await discoverAppRoutes(),
152
170
  });
@@ -1,96 +1,82 @@
1
1
  import React from "react";
2
+ import type { Fact, MockRow } from "@/lib/site.config";
2
3
 
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.
4
+ // Presentational pieces for the landing page. All server-rendered, no client
5
+ // JS. Colors come from CSS vars set on <html> in app/layout.tsx, which read
6
+ // lib/site.config.ts.
7
7
 
8
- // Shared container: a contained, centered column.
9
- export const WRAP = "mx-auto w-full max-w-3xl px-6";
8
+ // Shared container: the page is left-aligned inside a wide column.
9
+ export const WRAP = "mx-auto w-full max-w-6xl px-6";
10
10
 
11
- export function Eyebrow({ children }: { children: React.ReactNode }) {
11
+ // Three short facts in a row, a thin rule above each.
12
+ export function FactList({ items }: { items: Fact[] }) {
12
13
  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>
14
+ <ul className="grid gap-x-8 gap-y-10 sm:grid-cols-3">
15
+ {items.map((f) => (
16
+ <li key={f.title} className="border-t border-line pt-5">
17
+ <h3 className="font-display text-[17px] font-medium text-chalk">{f.title}</h3>
18
+ <p className="mt-2 text-[14px] leading-relaxed text-chalk-2">{f.body}</p>
19
+ </li>
20
+ ))}
21
+ </ul>
58
22
  );
59
23
  }
60
24
 
61
- // A grid of value props — icon + title + body.
62
- export function FeatureGrid({
63
- items,
25
+ // A product window drawn with divs: a title bar, a sidebar, and task rows.
26
+ // No screenshot. Content comes from `siteConfig.mock`.
27
+ export function ProductMock({
28
+ windowTitle,
29
+ sidebar,
30
+ activeSidebarIndex,
31
+ rows,
64
32
  }: {
65
- items: { title: string; body: string; icon?: string }[];
33
+ windowTitle: string;
34
+ sidebar: string[];
35
+ activeSidebarIndex: number;
36
+ rows: MockRow[];
66
37
  }) {
67
38
  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>
39
+ <div
40
+ aria-hidden
41
+ className="overflow-hidden rounded-lg border border-line bg-paper text-[12px] text-chalk-2"
42
+ >
43
+ <div className="flex h-9 items-center gap-2 border-b border-line px-3">
44
+ <span className="size-2.5 rounded-full bg-line" />
45
+ <span className="size-2.5 rounded-full bg-line" />
46
+ <span className="size-2.5 rounded-full bg-line" />
47
+ <span className="ml-3 font-display text-[12px] font-medium text-chalk">{windowTitle}</span>
48
+ </div>
49
+ <div className="grid grid-cols-[112px_1fr] sm:grid-cols-[140px_1fr]">
50
+ <div className="border-r border-line py-2">
51
+ {sidebar.map((item, i) => (
52
+ <div
53
+ key={item}
54
+ className={
55
+ i === activeSidebarIndex
56
+ ? "mx-2 rounded-md bg-ink-2 px-2 py-1.5 text-chalk"
57
+ : "mx-2 px-2 py-1.5"
58
+ }
59
+ >
60
+ {item}
61
+ </div>
62
+ ))}
82
63
  </div>
83
- ))}
64
+ <div className="divide-y divide-line">
65
+ {rows.map((r) => (
66
+ <div key={r.title} className="flex items-center gap-3 px-3 py-2.5">
67
+ <span
68
+ className={
69
+ r.done
70
+ ? "size-3.5 shrink-0 rounded-sm bg-brand"
71
+ : "size-3.5 shrink-0 rounded-sm border border-chalk-2/60"
72
+ }
73
+ />
74
+ <span className={r.done ? "text-chalk-2 line-through" : "text-chalk"}>{r.title}</span>
75
+ <span className="ml-auto font-mono-ui text-[11px] text-chalk-2">{r.tag}</span>
76
+ </div>
77
+ ))}
78
+ </div>
79
+ </div>
84
80
  </div>
85
81
  );
86
82
  }
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
- }
@@ -22,21 +22,26 @@ export type BaseConfig = {
22
22
  socials: Social[];
23
23
  };
24
24
  // Marketing accent + surfaces. Applied as CSS vars on <html> in layout.tsx.
25
- colors: { brand: string; brandSoft: string; paper: string };
25
+ // `ink` is the page background, `paper` the raised panel color, `brand` the
26
+ // one accent, `brandSoft` the accent's dim fill.
27
+ colors: { brand: string; brandSoft: string; paper: string; ink: string };
26
28
  seo: { title: string; description: string };
27
29
  };
28
30
 
29
- export type ValueProp = { title: string; body: string; icon?: string };
30
- export type Quote = { quote: string; name: string; role: string };
31
+ export type Fact = { title: string; body: string };
31
32
  export type Faq = { q: string; a: string };
32
33
 
34
+ // A row in the product mock (a task or a note), drawn with plain divs.
35
+ export type MockRow = { title: string; tag: string; done?: boolean };
36
+
33
37
  export type WaitlistConfig = BaseConfig & {
34
38
  hero: {
35
- badge: string;
39
+ launchNote: string; // one short line, set in mono: when it ships
36
40
  headline: string;
37
41
  subcopy: string;
38
42
  emailPlaceholder: string;
39
43
  ctaLabel: string;
44
+ formHint: string;
40
45
  successMessage: string;
41
46
  };
42
47
  counter: {
@@ -46,13 +51,15 @@ export type WaitlistConfig = BaseConfig & {
46
51
  // page doesn't read "0". Set to 0 to show only genuine signups.
47
52
  seedCount?: number;
48
53
  };
49
- valueProps: { eyebrow: string; headline: string; items: ValueProp[] };
50
- socialProof?: {
51
- label: string;
52
- logos?: string[];
53
- quotes?: Quote[];
54
+ // The HTML/CSS product window on the right of the hero. No screenshot.
55
+ mock: {
56
+ windowTitle: string;
57
+ sidebar: string[];
58
+ activeSidebarIndex: number;
59
+ rows: MockRow[];
54
60
  };
55
- faq?: { eyebrow: string; headline: string; items: Faq[] };
61
+ facts: { headline: string; items: Fact[] };
62
+ faq?: { headline: string; items: Faq[] };
56
63
  };
57
64
 
58
65
  /* ----------------------------- config ---------------------------- */
@@ -64,7 +71,7 @@ export const siteConfig: WaitlistConfig = {
64
71
  domain: "lumo.app",
65
72
  email: "hello@lumo.example",
66
73
  footerBlurb:
67
- "Lumo keeps projects, notes, and tasks in one focused workspace. Join the list for an early invite.",
74
+ "Lumo keeps projects, notes, and tasks in one workspace. Join the list for an early invite.",
68
75
  copyrightName: "Lumo, Inc.",
69
76
  socials: [
70
77
  {
@@ -80,95 +87,81 @@ export const siteConfig: WaitlistConfig = {
80
87
  ],
81
88
  },
82
89
 
83
- colors: { brand: "#4f46e5", brandSoft: "#eef2ff", paper: "#fafafa" },
90
+ // Near-black page, one electric blue accent.
91
+ colors: { brand: "#4f6bff", brandSoft: "#141a3d", paper: "#121214", ink: "#0b0b0c" },
84
92
 
85
93
  seo: {
86
- title: "Lumo — the calm home for your work. Coming soon.",
94
+ title: "Lumo — projects, notes, and tasks in one workspace. Coming soon.",
87
95
  description:
88
- "Lumo brings your projects, notes, and tasks into one quiet, focused space. We're launching soon — join the waitlist for an early invite.",
96
+ "Lumo puts your projects, notes, and tasks in one workspace. It launches this fall. Join the waitlist for an early invite.",
89
97
  },
90
98
 
91
99
  hero: {
92
- badge: "Launching this fall",
93
- headline: "Projects, notes, and tasks in one calm place.",
100
+ launchNote: "Launch: fall 2026",
101
+ headline: "Projects, notes, and tasks in one workspace.",
94
102
  subcopy:
95
- "Lumo keeps your projects, notes, and tasks together in a focused workspace. Join the list for an early invite.",
103
+ "Lumo is one app for the plan, the notes behind it, and the tasks that come out of it. Join the list and we send an invite before the public launch.",
96
104
  emailPlaceholder: "you@work.com",
97
105
  ctaLabel: "Join the waitlist",
98
- successMessage: "You're on the list — we'll email your invite soon.",
106
+ formHint: "One invite email. No newsletter.",
107
+ successMessage: "You are on the list. We will email your invite.",
99
108
  },
100
109
 
101
110
  counter: {
102
111
  enabled: true,
103
- label: "people already waiting",
112
+ label: "people on the list",
104
113
  seedCount: 1200,
105
114
  },
106
115
 
107
- valueProps: {
108
- eyebrow: "Why Lumo",
109
- headline: "Keep your attention on the work.",
110
- items: [
111
- {
112
- icon: "◇",
113
- title: "One quiet space",
114
- body: "Projects, notes, and tasks live together — so nothing gets lost between five different apps.",
115
- },
116
- {
117
- icon: "◎",
118
- title: "For people who make things",
119
- body: "Designers, founders, writers, builders. If your work needs deep focus, Lumo is built for you.",
120
- },
121
- {
122
- icon: "✦",
123
- title: "Calm by default",
124
- body: "Lumo keeps notifications and feeds quiet, then shows the work that needs attention now.",
125
- },
116
+ mock: {
117
+ windowTitle: "Site redesign",
118
+ sidebar: ["Inbox", "Site redesign", "Q4 plan", "Reading", "Archive"],
119
+ activeSidebarIndex: 1,
120
+ rows: [
121
+ { title: "Draft the homepage copy", tag: "Today", done: true },
122
+ { title: "Review nav on mobile", tag: "Today" },
123
+ { title: "Pick the display face", tag: "Note" },
124
+ { title: "Send pricing page to Priya", tag: "Tomorrow" },
125
+ { title: "Set up the launch checklist", tag: "Next week" },
126
126
  ],
127
127
  },
128
128
 
129
- socialProof: {
130
- label: "Loved by early testers",
131
- quotes: [
129
+ facts: {
130
+ headline: "What Lumo does",
131
+ items: [
132
132
  {
133
- quote:
134
- "I've tried every productivity app out there. Lumo is the first one that leaves me calmer after planning my week.",
135
- name: "Maya Chen",
136
- role: "Product designer",
133
+ title: "One place for a project",
134
+ body: "Each project holds its plan, its notes, and its tasks. Nothing lives in a second app.",
137
135
  },
138
136
  {
139
- quote:
140
- "All my half-finished projects are in one place. I check it every morning and know what to do next.",
141
- name: "Daniel Reyes",
142
- role: "Indie founder",
137
+ title: "Notes turn into tasks",
138
+ body: "Select a line in a note and make it a task. The task keeps a link back to the note.",
143
139
  },
144
140
  {
145
- quote:
146
- "It's fast, it's quiet, and it stays out of my way. I joined the waitlist the day I saw the first demo.",
147
- name: "Hannah Brooks",
148
- role: "Writer",
141
+ title: "No feed, no badges",
142
+ body: "Lumo has no activity feed. You see the tasks due today and the project you opened last.",
149
143
  },
150
144
  ],
151
145
  },
152
146
 
153
147
  faq: {
154
- eyebrow: "Questions",
155
- headline: "Good to know.",
148
+ headline: "Questions",
156
149
  items: [
157
150
  {
158
151
  q: "When does Lumo launch?",
159
- a: "We're aiming for this fall. Waitlist members get an invite before the public launch.",
152
+ a: "Fall 2026. People on the waitlist get an invite before the public launch.",
160
153
  },
161
154
  {
162
155
  q: "What do I get for joining?",
163
- a: "An early invite, founding-member pricing, and a direct line to shape what we build next.",
156
+ a: "An early invite and founding-member pricing on paid plans.",
164
157
  },
165
158
  {
166
159
  q: "How much will it cost?",
167
- a: "There'll be a generous free tier. Waitlist members lock in founding-member pricing on paid plans.",
160
+ a: "There is a free tier. Paid plans are priced at launch. Waitlist members keep the founding-member price.",
168
161
  },
169
162
  {
170
163
  q: "Will my email be shared?",
171
- a: "Never. We only use it to send your invite and the occasional launch update. Unsubscribe anytime.",
164
+ a: "No. We use it for the invite and launch updates only. You can unsubscribe at any time.",
172
165
  },
173
166
  ],
174
167
  },
@@ -1,159 +0,0 @@
1
- "use client";
2
-
3
- import React, { useEffect, useState } from "react";
4
- import { db, callFn } from "@pylonsync/react";
5
- import { EnsureGuest, useAuth } from "@pylonsync/client";
6
- import { Button } from "@/components/ui/button";
7
-
8
- export interface Post {
9
- id: string;
10
- authorId: string;
11
- text: string;
12
- createdAt: string;
13
- }
14
-
15
- export interface Like {
16
- id: string;
17
- userId: string;
18
- postId: string;
19
- }
20
-
21
- // `<EnsureGuest>` mints a guest session so anyone can post + like with no
22
- // login. Inside it, `useAuth()` exposes the guest's `userId` — that's how we
23
- // know which posts the current visitor has already liked.
24
- export function Feed() {
25
- return (
26
- <EnsureGuest fallback={<FeedSkeleton />}>
27
- <FeedInner />
28
- </EnsureGuest>
29
- );
30
- }
31
-
32
- function FeedInner() {
33
- const { userId } = useAuth();
34
- const [text, setText] = useState("");
35
- // Seed a few demo posts on first visit so the feed isn't empty. No-op once
36
- // any post exists; safe to call every mount.
37
- useEffect(() => {
38
- void callFn("seedPosts", {});
39
- }, []);
40
- const { data: posts, loading } = db.useQuery<Post>("Post");
41
- // Public-read, so this is every like in the system — we count + match them
42
- // client-side. db.useQuery is live, so counts update the instant anyone likes.
43
- const { data: likes } = db.useQuery<Like>("Like");
44
-
45
- async function addPost(e: React.FormEvent) {
46
- e.preventDefault();
47
- const value = text.trim();
48
- if (!value) return;
49
- setText("");
50
- // No authorId sent — field.owner() stamps it from the session.
51
- await db.insert("Post", { text: value });
52
- }
53
-
54
- function toggleLike(postId: string) {
55
- const mine = likes.find((l) => l.postId === postId && l.userId === userId);
56
- if (mine) {
57
- void db.delete("Like", mine.id);
58
- } else {
59
- void db.insert("Like", { postId });
60
- }
61
- }
62
-
63
- const ordered = [...posts].sort((a, b) =>
64
- b.createdAt.localeCompare(a.createdAt),
65
- );
66
-
67
- return (
68
- <div className="space-y-6">
69
- <form onSubmit={addPost} className="space-y-2">
70
- <textarea
71
- value={text}
72
- onChange={(e) => setText(e.target.value)}
73
- placeholder="What's happening?"
74
- aria-label="Post"
75
- rows={2}
76
- className="w-full resize-none rounded-md border border-input bg-transparent px-3 py-2 text-sm shadow-sm outline-none focus-visible:ring-1 focus-visible:ring-ring"
77
- />
78
- <div className="flex justify-end">
79
- <Button type="submit" disabled={!text.trim()}>
80
- Post
81
- </Button>
82
- </div>
83
- </form>
84
-
85
- {loading && posts.length === 0 ? (
86
- <FeedSkeleton />
87
- ) : ordered.length === 0 ? (
88
- <p className="text-sm text-muted-foreground">
89
- No posts yet — say something above. It appears instantly and syncs;
90
- open a second tab to watch the feed update live.
91
- </p>
92
- ) : (
93
- <ul className="space-y-3">
94
- {ordered.map((post) => {
95
- const count = likes.filter((l) => l.postId === post.id).length;
96
- const liked = likes.some(
97
- (l) => l.postId === post.id && l.userId === userId,
98
- );
99
- const mine = post.authorId === userId;
100
- return (
101
- <li key={post.id} className="rounded-lg border px-4 py-3">
102
- <div className="flex items-center justify-between text-xs text-muted-foreground">
103
- <span className="font-mono">
104
- {mine ? "you" : shortId(post.authorId)}
105
- </span>
106
- {mine && (
107
- <button
108
- type="button"
109
- aria-label="Delete post"
110
- onClick={() => db.delete("Post", post.id)}
111
- className="hover:text-red-600"
112
- >
113
- Delete
114
- </button>
115
- )}
116
- </div>
117
- <p className="mt-1 whitespace-pre-wrap text-sm">{post.text}</p>
118
- <div className="mt-2">
119
- <button
120
- type="button"
121
- onClick={() => toggleLike(post.id)}
122
- aria-pressed={liked}
123
- className={
124
- "inline-flex items-center gap-1.5 text-xs transition-colors " +
125
- (liked
126
- ? "text-rose-600"
127
- : "text-muted-foreground hover:text-foreground")
128
- }
129
- >
130
- <span>{liked ? "♥" : "♡"}</span>
131
- <span>{count}</span>
132
- </button>
133
- </div>
134
- </li>
135
- );
136
- })}
137
- </ul>
138
- )}
139
- </div>
140
- );
141
- }
142
-
143
- // Guest ids look like `guest_<hex>`; show a short, stable handle.
144
- function shortId(id: string) {
145
- return "@" + id.replace(/^guest_/, "").slice(0, 6);
146
- }
147
-
148
- function FeedSkeleton() {
149
- return (
150
- <ul className="space-y-3" aria-hidden>
151
- {[0, 1, 2].map((i) => (
152
- <li key={i} className="rounded-lg border px-4 py-3">
153
- <span className="block h-3 w-16 rounded bg-muted" />
154
- <span className="mt-2 block h-4 w-3/4 rounded bg-muted" />
155
- </li>
156
- ))}
157
- </ul>
158
- );
159
- }
@@ -1,27 +0,0 @@
1
- import React from "react";
2
- import { type Metadata } from "@pylonsync/react";
3
- import { Feed } from "./feed-client";
4
-
5
- export const metadata: Metadata = {
6
- title: "__APP_NAME__ — a live social feed on Pylon",
7
- description:
8
- "A public feed with optimistic posts and likes, server-rendered over one Pylon backend. Open two tabs and watch it sync.",
9
- };
10
-
11
- // `app/page.tsx` → `/`. The intro is server-rendered; `<Feed>` is a client
12
- // island that mints a guest session and runs the live query + optimistic
13
- // posts/likes in the browser.
14
- export default function IndexPage() {
15
- return (
16
- <div>
17
- <header className="mb-6">
18
- <h1 className="text-2xl font-semibold tracking-tight">Feed</h1>
19
- <p className="mt-1 text-sm text-muted-foreground">
20
- Post something — it appears instantly and syncs to every tab. Likes
21
- are live too.
22
- </p>
23
- </header>
24
- <Feed />
25
- </div>
26
- );
27
- }
@@ -1,13 +0,0 @@
1
- // Server functions go here. Each file in this directory that exports a
2
- // query() or action() becomes a typed RPC endpoint, callable from your
3
- // pages and client with full type inference. Delete this placeholder when
4
- // you add your first one.
5
- //
6
- // Example (functions/notes.ts):
7
- //
8
- // import { query } from "@pylonsync/functions";
9
- //
10
- // export const listNotes = query(async (ctx) => {
11
- // return ctx.db.list("Note");
12
- // });
13
- export {};
@@ -1,56 +0,0 @@
1
- import { mutation } from "@pylonsync/functions";
2
-
3
- // A few demo posts so the feed isn't an empty shell on first visit. The feed
4
- // calls this on mount; it's a no-op once any post exists (the lock guards
5
- // against a double-seed from two concurrent first-visits). Public so an
6
- // anonymous first visitor seeds it — it only writes demo content.
7
- //
8
- // `unsafe.insert` sets the demo `authorId` + a backdated `createdAt` directly
9
- // (bypassing the `field.owner()` stamp + policies) so the seed reads like a few
10
- // different people already posted, instead of everything attributed to the
11
- // first visitor.
12
- const DEMO_POSTS: { author: string; text: string }[] = [
13
- {
14
- author: "guest_ada",
15
- text: "just shipped my first Pylon app — one binary doing SSR + sync + auth. wild.",
16
- },
17
- {
18
- author: "guest_lin",
19
- text: "the feed updates across tabs with zero websocket code I wrote. open a second tab 👀",
20
- },
21
- {
22
- author: "guest_rey",
23
- text: "likes are just rows — delete the row to unlike. local-first, so it's instant.",
24
- },
25
- {
26
- author: "guest_max",
27
- text: "no separate backend to deploy. `pylon deploy` and it's live. that's the whole thing.",
28
- },
29
- ];
30
-
31
- export default mutation<
32
- Record<string, never>,
33
- { seeded: boolean; count: number }
34
- >({
35
- auth: "public",
36
- async handler(ctx) {
37
- await ctx.db.advisoryLock("consumer_seed_posts");
38
- const existing = await ctx.db.unsafe.list("Post");
39
- if (existing.length > 0) return { seeded: false, count: existing.length };
40
-
41
- const now = Date.now();
42
- for (let i = 0; i < DEMO_POSTS.length; i++) {
43
- const p = DEMO_POSTS[i];
44
- // Backdate a minute apart so they sort into a natural order.
45
- const createdAt = new Date(
46
- now - (DEMO_POSTS.length - i) * 60_000,
47
- ).toISOString();
48
- await ctx.db.unsafe.insert("Post", {
49
- authorId: p.author,
50
- text: p.text,
51
- createdAt,
52
- });
53
- }
54
- return { seeded: true, count: DEMO_POSTS.length };
55
- },
56
- });
@@ -1,12 +0,0 @@
1
- import { expect, test } from "bun:test";
2
-
3
- // Your first test. Run the suite with: pylon test (or: npm test)
4
- //
5
- // `pylon test` discovers every *.test.ts file under tests/ (or functions/) and
6
- // runs it with Bun's test runner against an in-memory Pylon. Replace this
7
- // placeholder with tests for your functions and helpers — see AGENTS.md →
8
- // Testing for the pure-logic and HTTP-against-`pylon dev` patterns.
9
-
10
- test("placeholder — replace me with a real test", () => {
11
- expect(true).toBe(true);
12
- });