@pylonsync/create-pylon 0.3.268 → 0.3.270
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.
- package/bin/create-pylon.js +11 -9
- package/package.json +1 -1
- package/templates/b2b/app/layout.tsx +1 -1
- package/templates/b2b/app/page.tsx +2 -2
- package/templates/b2b/tsconfig.json +1 -1
- package/templates/barebones/app/page.tsx +1 -1
- package/templates/barebones/tsconfig.json +1 -1
- package/templates/chat/app/page.tsx +1 -1
- package/templates/chat/tsconfig.json +1 -1
- package/templates/consumer/app/page.tsx +1 -1
- package/templates/consumer/tsconfig.json +1 -1
- package/templates/default/.env.example +19 -0
- package/templates/default/README.md +85 -0
- package/templates/default/app/auth-form.tsx +218 -0
- package/templates/default/app/auth-shell.tsx +76 -0
- package/templates/default/app/company/[slug]/page.tsx +28 -0
- package/templates/default/app/compare/[slug]/page.tsx +27 -0
- package/templates/default/app/dashboard/billing/page.tsx +49 -0
- package/templates/default/app/dashboard/dashboard-client.tsx +832 -0
- package/templates/default/app/dashboard/members/page.tsx +37 -0
- package/templates/default/app/dashboard/page.tsx +64 -0
- package/templates/default/app/dashboard/projects/page.tsx +37 -0
- package/templates/default/app/dashboard/settings/page.tsx +45 -0
- package/templates/{ssr → default}/app/globals.css +14 -0
- package/templates/default/app/layout.tsx +466 -0
- package/templates/default/app/login/page.tsx +27 -0
- package/templates/default/app/onboarding/onboarding-client.tsx +261 -0
- package/templates/default/app/onboarding/page.tsx +29 -0
- package/templates/default/app/page.tsx +653 -0
- package/templates/default/app/products/[slug]/page.tsx +134 -0
- package/templates/default/app/resources/[slug]/page.tsx +28 -0
- package/templates/default/app/signup/page.tsx +24 -0
- package/templates/default/app/sitemap.ts +40 -0
- package/templates/default/app/solutions/[slug]/page.tsx +28 -0
- package/templates/default/app.ts +194 -0
- package/templates/default/components/dashboard-shell.tsx +150 -0
- package/templates/default/components/marketing.tsx +370 -0
- package/templates/default/functions/_pylonStripeFindActiveSubForReference.ts +3 -0
- package/templates/default/functions/_pylonStripeFindByCustomerId.ts +3 -0
- package/templates/default/functions/_pylonStripeGetCustomerHolder.ts +3 -0
- package/templates/default/functions/_pylonStripeListSubsForReference.ts +3 -0
- package/templates/default/functions/_pylonStripeOrgMembership.ts +3 -0
- package/templates/default/functions/_pylonStripeSetCustomerId.ts +3 -0
- package/templates/default/functions/_pylonStripeUpsertSubscription.ts +3 -0
- package/templates/default/functions/cancelSubscription.ts +3 -0
- package/templates/default/functions/createBillingPortalSession.ts +3 -0
- package/templates/default/functions/createCheckoutSession.ts +3 -0
- package/templates/default/functions/restoreSubscription.ts +3 -0
- package/templates/default/functions/stripeWebhook.ts +3 -0
- package/templates/default/lib/billing.ts +46 -0
- package/templates/default/lib/products.ts +122 -0
- package/templates/default/lib/site.ts +261 -0
- package/templates/{ssr → default}/package.json +2 -0
- package/templates/{ssr → default}/tsconfig.json +2 -2
- package/templates/todo/app/page.tsx +1 -1
- package/templates/todo/tsconfig.json +1 -1
- package/templates/ssr/README.md +0 -56
- package/templates/ssr/app/auth-form.tsx +0 -142
- package/templates/ssr/app/dashboard/dashboard-client.tsx +0 -116
- package/templates/ssr/app/dashboard/page.tsx +0 -70
- package/templates/ssr/app/layout.tsx +0 -71
- package/templates/ssr/app/login/page.tsx +0 -47
- package/templates/ssr/app/page.tsx +0 -114
- package/templates/ssr/app/signup/page.tsx +0 -44
- package/templates/ssr/app/sitemap.ts +0 -27
- package/templates/ssr/app.ts +0 -94
- package/templates/ssr/functions/_keep.ts +0 -13
- /package/templates/{ssr → default}/AGENTS.md +0 -0
- /package/templates/{ssr → default}/app/error.tsx +0 -0
- /package/templates/{ssr → default}/app/not-found.tsx +0 -0
- /package/templates/{ssr → default}/app/robots.ts +0 -0
- /package/templates/{ssr → default}/components/ui/button.tsx +0 -0
- /package/templates/{ssr → default}/components/ui/card.tsx +0 -0
- /package/templates/{ssr → default}/components.json +0 -0
- /package/templates/{ssr → default}/gitignore +0 -0
- /package/templates/{ssr → default}/lib/utils.ts +0 -0
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
// Content for the rest of the marketing site — solutions, resources, company,
|
|
2
|
+
// and comparison pages. Each collection drives a dynamic route AND the footer
|
|
3
|
+
// columns, so the links and the pages can never drift. Fictional demo copy —
|
|
4
|
+
// swap it for your own.
|
|
5
|
+
|
|
6
|
+
export type ContentSection = { title: string; body: string };
|
|
7
|
+
|
|
8
|
+
export type SitePage = {
|
|
9
|
+
slug: string;
|
|
10
|
+
navLabel: string; // label in nav/footer
|
|
11
|
+
eyebrow: string;
|
|
12
|
+
title: string; // hero headline
|
|
13
|
+
summary: string;
|
|
14
|
+
sections: ContentSection[];
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export const SOLUTIONS: SitePage[] = [
|
|
18
|
+
{
|
|
19
|
+
slug: "startups",
|
|
20
|
+
navLabel: "For startups",
|
|
21
|
+
eyebrow: "Solutions",
|
|
22
|
+
title: "Move fast without losing the thread.",
|
|
23
|
+
summary:
|
|
24
|
+
"Keep a small team aligned as everything changes weekly. Acme gives you one place to plan, build, and ship before the next pivot.",
|
|
25
|
+
sections: [
|
|
26
|
+
{ title: "One tool, not ten", body: "Projects, tasks, and docs in one place, so you are not paying for or stitching together five apps." },
|
|
27
|
+
{ title: "Set up in minutes", body: "No admin overhead. Invite the team and start working the same day." },
|
|
28
|
+
{ title: "Grows with you", body: "The same workspace works at five people and at fifty." },
|
|
29
|
+
],
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
slug: "agencies",
|
|
33
|
+
navLabel: "For agencies",
|
|
34
|
+
eyebrow: "Solutions",
|
|
35
|
+
title: "Run every client like clockwork.",
|
|
36
|
+
summary:
|
|
37
|
+
"Give each client their own space, keep the work organized, and show progress without a status meeting.",
|
|
38
|
+
sections: [
|
|
39
|
+
{ title: "A space per client", body: "Separate workspaces keep every engagement tidy and private." },
|
|
40
|
+
{ title: "Shareable views", body: "Send clients a read-only view of exactly what is in flight." },
|
|
41
|
+
{ title: "Reusable templates", body: "Start every new engagement from a proven playbook." },
|
|
42
|
+
],
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
slug: "enterprise",
|
|
46
|
+
navLabel: "For enterprise",
|
|
47
|
+
eyebrow: "Solutions",
|
|
48
|
+
title: "Scale without the chaos.",
|
|
49
|
+
summary:
|
|
50
|
+
"Bring hundreds of people into one system of record, with the controls and visibility a larger org needs.",
|
|
51
|
+
sections: [
|
|
52
|
+
{ title: "SSO and roles", body: "Single sign-on and granular roles keep access where it belongs." },
|
|
53
|
+
{ title: "Audit log", body: "A complete record of who changed what, and when." },
|
|
54
|
+
{ title: "Rollups", body: "See progress across teams and departments in one view." },
|
|
55
|
+
],
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
slug: "teams",
|
|
59
|
+
navLabel: "For teams",
|
|
60
|
+
eyebrow: "Solutions",
|
|
61
|
+
title: "Built for how your team works.",
|
|
62
|
+
summary:
|
|
63
|
+
"Whether you build, design, market, or support, Acme adapts to your process instead of forcing a new one.",
|
|
64
|
+
sections: [
|
|
65
|
+
{ title: "Your workflow", body: "Custom statuses and fields match the way your team already works." },
|
|
66
|
+
{ title: "Cross-team work", body: "Hand work between teams without it falling through a crack." },
|
|
67
|
+
{ title: "Less status-chasing", body: "Everyone sees the same live picture, so updates write themselves." },
|
|
68
|
+
],
|
|
69
|
+
},
|
|
70
|
+
];
|
|
71
|
+
|
|
72
|
+
export const RESOURCES: SitePage[] = [
|
|
73
|
+
{
|
|
74
|
+
slug: "docs",
|
|
75
|
+
navLabel: "Docs",
|
|
76
|
+
eyebrow: "Resources",
|
|
77
|
+
title: "Documentation.",
|
|
78
|
+
summary: "Everything you need to set up Acme and get your team productive.",
|
|
79
|
+
sections: [
|
|
80
|
+
{ title: "Getting started", body: "Create a workspace, invite your team, and ship your first project." },
|
|
81
|
+
{ title: "Guides", body: "Deep dives on projects, tasks, docs, automations, and analytics." },
|
|
82
|
+
{ title: "API", body: "Build on the typed Acme API and webhooks." },
|
|
83
|
+
],
|
|
84
|
+
},
|
|
85
|
+
{
|
|
86
|
+
slug: "guides",
|
|
87
|
+
navLabel: "Guides",
|
|
88
|
+
eyebrow: "Resources",
|
|
89
|
+
title: "Guides and playbooks.",
|
|
90
|
+
summary: "Practical walkthroughs for getting the most out of Acme.",
|
|
91
|
+
sections: [
|
|
92
|
+
{ title: "Run a sprint", body: "Plan, track, and review a two-week cycle in Acme." },
|
|
93
|
+
{ title: "Automate intake", body: "Route incoming work to the right team automatically." },
|
|
94
|
+
{ title: "Report to leadership", body: "Build a dashboard that answers the questions you get asked." },
|
|
95
|
+
],
|
|
96
|
+
},
|
|
97
|
+
{
|
|
98
|
+
slug: "changelog",
|
|
99
|
+
navLabel: "Changelog",
|
|
100
|
+
eyebrow: "Resources",
|
|
101
|
+
title: "What's new.",
|
|
102
|
+
summary: "Every improvement we ship, in one place.",
|
|
103
|
+
sections: [
|
|
104
|
+
{ title: "This week", body: "Faster search, a redesigned task list, and new automation triggers." },
|
|
105
|
+
{ title: "Last week", body: "Timeline view for projects and CSV export for analytics." },
|
|
106
|
+
{ title: "Earlier", body: "Webhooks, custom fields, and version history for docs." },
|
|
107
|
+
],
|
|
108
|
+
},
|
|
109
|
+
{
|
|
110
|
+
slug: "api",
|
|
111
|
+
navLabel: "API reference",
|
|
112
|
+
eyebrow: "Resources",
|
|
113
|
+
title: "API reference.",
|
|
114
|
+
summary: "A typed REST API and webhooks for everything in Acme.",
|
|
115
|
+
sections: [
|
|
116
|
+
{ title: "Authentication", body: "API keys scoped to a workspace, revocable at any time." },
|
|
117
|
+
{ title: "Resources", body: "Projects, tasks, docs, and automations, all over the same API." },
|
|
118
|
+
{ title: "Webhooks", body: "Subscribe to events and react to changes in real time." },
|
|
119
|
+
],
|
|
120
|
+
},
|
|
121
|
+
{
|
|
122
|
+
slug: "status",
|
|
123
|
+
navLabel: "Status",
|
|
124
|
+
eyebrow: "Resources",
|
|
125
|
+
title: "System status.",
|
|
126
|
+
summary: "Live status for every Acme service.",
|
|
127
|
+
sections: [
|
|
128
|
+
{ title: "API", body: "Operational — 99.99% over the last 90 days." },
|
|
129
|
+
{ title: "Web app", body: "Operational — no incidents this week." },
|
|
130
|
+
{ title: "Webhooks", body: "Operational — delivering within seconds." },
|
|
131
|
+
],
|
|
132
|
+
},
|
|
133
|
+
];
|
|
134
|
+
|
|
135
|
+
export const COMPANY: SitePage[] = [
|
|
136
|
+
{
|
|
137
|
+
slug: "about",
|
|
138
|
+
navLabel: "About",
|
|
139
|
+
eyebrow: "Company",
|
|
140
|
+
title: "About Acme.",
|
|
141
|
+
summary: "We build the workspace we always wanted: fast, focused, and a pleasure to use.",
|
|
142
|
+
sections: [
|
|
143
|
+
{ title: "Our mission", body: "Help teams do their best work without fighting their tools." },
|
|
144
|
+
{ title: "How we work", body: "Small team, weekly releases, every decision close to the user." },
|
|
145
|
+
{ title: "Where we are", body: "Remote-first, with people across a dozen time zones." },
|
|
146
|
+
],
|
|
147
|
+
},
|
|
148
|
+
{
|
|
149
|
+
slug: "blog",
|
|
150
|
+
navLabel: "Blog",
|
|
151
|
+
eyebrow: "Company",
|
|
152
|
+
title: "The Acme blog.",
|
|
153
|
+
summary: "Notes on building Acme, and on building product in general.",
|
|
154
|
+
sections: [
|
|
155
|
+
{ title: "Why one tool beats ten", body: "The hidden cost of stitching your stack together." },
|
|
156
|
+
{ title: "Shipping weekly", body: "How a small team keeps a steady release cadence." },
|
|
157
|
+
{ title: "Designing for focus", body: "The principles behind the Acme interface." },
|
|
158
|
+
],
|
|
159
|
+
},
|
|
160
|
+
{
|
|
161
|
+
slug: "careers",
|
|
162
|
+
navLabel: "Careers",
|
|
163
|
+
eyebrow: "Company",
|
|
164
|
+
title: "Work at Acme.",
|
|
165
|
+
summary: "We are a small team that ships a lot. If that sounds good, come build with us.",
|
|
166
|
+
sections: [
|
|
167
|
+
{ title: "Engineering", body: "Full-stack engineers who care about craft and speed." },
|
|
168
|
+
{ title: "Design", body: "Product designers who sweat the details." },
|
|
169
|
+
{ title: "Support", body: "People who love helping customers succeed." },
|
|
170
|
+
],
|
|
171
|
+
},
|
|
172
|
+
{
|
|
173
|
+
slug: "contact",
|
|
174
|
+
navLabel: "Contact",
|
|
175
|
+
eyebrow: "Company",
|
|
176
|
+
title: "Get in touch.",
|
|
177
|
+
summary: "Questions, feedback, or just want to say hi? We would love to hear from you.",
|
|
178
|
+
sections: [
|
|
179
|
+
{ title: "Sales", body: "Talk through whether Acme is a fit for your team." },
|
|
180
|
+
{ title: "Support", body: "Get help from a human, usually within a few hours." },
|
|
181
|
+
{ title: "Press", body: "Logos, screenshots, and company facts for the press." },
|
|
182
|
+
],
|
|
183
|
+
},
|
|
184
|
+
{
|
|
185
|
+
slug: "privacy",
|
|
186
|
+
navLabel: "Privacy",
|
|
187
|
+
eyebrow: "Company",
|
|
188
|
+
title: "Privacy.",
|
|
189
|
+
summary: "How Acme handles your data, in plain language.",
|
|
190
|
+
sections: [
|
|
191
|
+
{ title: "What we collect", body: "Only what we need to run the product and support you." },
|
|
192
|
+
{ title: "How we use it", body: "To operate Acme — never sold, never rented." },
|
|
193
|
+
{ title: "Your control", body: "Export or delete your data at any time." },
|
|
194
|
+
],
|
|
195
|
+
},
|
|
196
|
+
];
|
|
197
|
+
|
|
198
|
+
export type Comparison = {
|
|
199
|
+
slug: string;
|
|
200
|
+
navLabel: string;
|
|
201
|
+
competitor: string;
|
|
202
|
+
title: string;
|
|
203
|
+
summary: string;
|
|
204
|
+
rows: { dim: string; acme: string; them: string }[];
|
|
205
|
+
};
|
|
206
|
+
|
|
207
|
+
// Generic, made-up competitors so the template ships no real brand names.
|
|
208
|
+
export const COMPARISONS: Comparison[] = [
|
|
209
|
+
{
|
|
210
|
+
slug: "beacon",
|
|
211
|
+
navLabel: "Acme vs Beacon",
|
|
212
|
+
competitor: "Beacon",
|
|
213
|
+
title: "Acme vs Beacon",
|
|
214
|
+
summary:
|
|
215
|
+
"Beacon is a capable tool, but it splits projects, docs, and automation across separate products. Acme brings them into one fast workspace.",
|
|
216
|
+
rows: [
|
|
217
|
+
{ dim: "Projects, tasks, and docs", acme: "In one workspace", them: "Separate products" },
|
|
218
|
+
{ dim: "Real-time sync", acme: "Built in", them: "Add-on" },
|
|
219
|
+
{ dim: "Automations", acme: "Included", them: "Higher tier" },
|
|
220
|
+
{ dim: "Typed API", acme: "Yes", them: "Partial" },
|
|
221
|
+
{ dim: "Setup time", acme: "Minutes", them: "Hours" },
|
|
222
|
+
],
|
|
223
|
+
},
|
|
224
|
+
{
|
|
225
|
+
slug: "orbit",
|
|
226
|
+
navLabel: "Acme vs Orbit",
|
|
227
|
+
competitor: "Orbit",
|
|
228
|
+
title: "Acme vs Orbit",
|
|
229
|
+
summary:
|
|
230
|
+
"Orbit is flexible but slow to set up and heavy to run. Acme gives you the same power with a fraction of the overhead.",
|
|
231
|
+
rows: [
|
|
232
|
+
{ dim: "Time to first project", acme: "Same day", them: "Onboarding required" },
|
|
233
|
+
{ dim: "Speed", acme: "Instant, real-time", them: "Page reloads" },
|
|
234
|
+
{ dim: "Per-seat pricing", acme: "No surprises", them: "Adds up fast" },
|
|
235
|
+
{ dim: "Analytics", acme: "Built in", them: "Separate tool" },
|
|
236
|
+
{ dim: "Learning curve", acme: "Gentle", them: "Steep" },
|
|
237
|
+
],
|
|
238
|
+
},
|
|
239
|
+
{
|
|
240
|
+
slug: "tempo",
|
|
241
|
+
navLabel: "Acme vs Tempo",
|
|
242
|
+
competitor: "Tempo",
|
|
243
|
+
title: "Acme vs Tempo",
|
|
244
|
+
summary:
|
|
245
|
+
"Tempo is built for managers; Acme is built for the whole team. Everyone gets a fast, shared view of the work.",
|
|
246
|
+
rows: [
|
|
247
|
+
{ dim: "Designed for", acme: "The whole team", them: "Managers" },
|
|
248
|
+
{ dim: "Daily driver", acme: "Yes", them: "Reporting layer" },
|
|
249
|
+
{ dim: "Docs included", acme: "Yes", them: "No" },
|
|
250
|
+
{ dim: "Automations", acme: "Included", them: "Limited" },
|
|
251
|
+
{ dim: "Self-serve", acme: "Yes", them: "Sales-led" },
|
|
252
|
+
],
|
|
253
|
+
},
|
|
254
|
+
];
|
|
255
|
+
|
|
256
|
+
export function bySlug<T extends { slug: string }>(
|
|
257
|
+
list: T[],
|
|
258
|
+
slug: string,
|
|
259
|
+
): T | undefined {
|
|
260
|
+
return list.find((x) => x.slug === slug);
|
|
261
|
+
}
|
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
"@pylonsync/sdk": "^__PYLON_VERSION__",
|
|
14
14
|
"@pylonsync/functions": "^__PYLON_VERSION__",
|
|
15
15
|
"@pylonsync/client": "^__PYLON_VERSION__",
|
|
16
|
+
"@pylonsync/stripe": "^__PYLON_VERSION__",
|
|
16
17
|
"react": "^19.0.0",
|
|
17
18
|
"react-dom": "^19.0.0",
|
|
18
19
|
"tailwindcss": "^4.3.0",
|
|
@@ -26,6 +27,7 @@
|
|
|
26
27
|
},
|
|
27
28
|
"devDependencies": {
|
|
28
29
|
"@pylonsync/cli": "^__PYLON_VERSION__",
|
|
30
|
+
"@types/node": "^22.0.0",
|
|
29
31
|
"@types/react": "^19.0.0",
|
|
30
32
|
"@types/react-dom": "^19.0.0",
|
|
31
33
|
"typescript": "^5.6.0"
|
|
@@ -8,11 +8,11 @@
|
|
|
8
8
|
"strict": true,
|
|
9
9
|
"skipLibCheck": true,
|
|
10
10
|
"lib": ["ES2022", "DOM"],
|
|
11
|
-
"types": ["react", "react-dom"],
|
|
11
|
+
"types": ["react", "react-dom", "node"],
|
|
12
12
|
"baseUrl": ".",
|
|
13
13
|
"paths": {
|
|
14
14
|
"@/*": ["./*"]
|
|
15
15
|
}
|
|
16
16
|
},
|
|
17
|
-
"include": ["app.ts", "app/**/*", "components/**/*", "lib/**/*"]
|
|
17
|
+
"include": ["app.ts", "app/**/*", "components/**/*", "lib/**/*", "functions/**/*"]
|
|
18
18
|
}
|
|
@@ -9,7 +9,7 @@ import { TodoApp } from "./todo-app";
|
|
|
9
9
|
export const metadata: Metadata = {
|
|
10
10
|
title: "__APP_NAME__ — a live Pylon todo",
|
|
11
11
|
description:
|
|
12
|
-
"A server-rendered todo list with live, optimistic, per-user sync
|
|
12
|
+
"A server-rendered todo list with live, optimistic, per-user sync. Open two tabs and watch them stay in sync.",
|
|
13
13
|
};
|
|
14
14
|
|
|
15
15
|
// `app/page.tsx` → `/`. The heading and intro are server-rendered (view
|
package/templates/ssr/README.md
DELETED
|
@@ -1,56 +0,0 @@
|
|
|
1
|
-
# __APP_NAME__
|
|
2
|
-
|
|
3
|
-
A full-stack [Pylon](https://pylonsync.com) app — a server-rendered homepage,
|
|
4
|
-
email/password auth, and a live client dashboard over a synced database, all
|
|
5
|
-
served from one binary on one port. No Next.js, no separate API server.
|
|
6
|
-
|
|
7
|
-
## Develop
|
|
8
|
-
|
|
9
|
-
```bash
|
|
10
|
-
__RUN_DEV__
|
|
11
|
-
```
|
|
12
|
-
|
|
13
|
-
Open http://localhost:4321. Sign up, and your notes dashboard updates live
|
|
14
|
-
(open a second tab to watch writes sync). Edit any file under `app/` and save —
|
|
15
|
-
the page reloads instantly.
|
|
16
|
-
|
|
17
|
-
## Layout
|
|
18
|
-
|
|
19
|
-
```
|
|
20
|
-
app.ts data model + manifest (entities, policies, auth, routes)
|
|
21
|
-
app/page.tsx "/" — the server-rendered, auth-aware homepage
|
|
22
|
-
app/login,signup/ email/password forms (POST /api/auth/password/*)
|
|
23
|
-
app/dashboard/ "/dashboard" — authed; server-gated, live notes + sign out
|
|
24
|
-
app/auth-form.tsx shared client island for the login/signup forms
|
|
25
|
-
app/layout.tsx root layout wrapping every page (auth-aware nav)
|
|
26
|
-
app/globals.css Tailwind entrypoint (compiled by Pylon)
|
|
27
|
-
functions/ server functions (query/mutation/action) — typed RPC
|
|
28
|
-
```
|
|
29
|
-
|
|
30
|
-
## How auth works
|
|
31
|
-
|
|
32
|
-
Email/password is built in. `/login` and `/signup` call
|
|
33
|
-
`/api/auth/password/*`; on success the server sets an **HttpOnly session
|
|
34
|
-
cookie** (no token in JS-readable storage). `/dashboard` reads `auth` during
|
|
35
|
-
the server render and redirects anonymous visitors to `/login` — a real 3xx
|
|
36
|
-
before any HTML, so there's no flash and it works with JS off. The sync engine
|
|
37
|
-
authenticates with the same cookie.
|
|
38
|
-
|
|
39
|
-
## Add a route
|
|
40
|
-
|
|
41
|
-
Drop a file at `app/about/page.tsx` and visit `/about`. Pages receive
|
|
42
|
-
`{ url, params, searchParams, auth, response, serverData }` from the SSR
|
|
43
|
-
runtime — all typed via `PageProps` from `@pylonsync/react`.
|
|
44
|
-
|
|
45
|
-
## Add data
|
|
46
|
-
|
|
47
|
-
Edit `app.ts`. Every `entity()` becomes a synced table with a REST +
|
|
48
|
-
realtime API and a typed client — no migrations, no resolvers.
|
|
49
|
-
|
|
50
|
-
## Deploy
|
|
51
|
-
|
|
52
|
-
```bash
|
|
53
|
-
pylon deploy
|
|
54
|
-
```
|
|
55
|
-
|
|
56
|
-
Docs: https://docs.pylonsync.com
|
|
@@ -1,142 +0,0 @@
|
|
|
1
|
-
"use client";
|
|
2
|
-
|
|
3
|
-
import React, { useState } from "react";
|
|
4
|
-
import { passwordLogin, passwordRegister, ApiError } from "@pylonsync/client";
|
|
5
|
-
import { Button } from "@/components/ui/button";
|
|
6
|
-
|
|
7
|
-
// The email/password form, shared by /login and /signup. It calls the built-in
|
|
8
|
-
// auth API directly — `passwordLogin` / `passwordRegister` (from
|
|
9
|
-
// @pylonsync/client) POST to `/api/auth/password/*`.
|
|
10
|
-
//
|
|
11
|
-
// On success the server sets an HttpOnly session cookie on the response. We do
|
|
12
|
-
// a full navigation to /dashboard rather than a client transition: the fresh
|
|
13
|
-
// page load hands that cookie to the SSR runtime (which resolves auth and
|
|
14
|
-
// renders the dashboard server-side) and to the sync engine (which
|
|
15
|
-
// authenticates with the same cookie via `credentials: include`). Because the
|
|
16
|
-
// cookie is HttpOnly it can never be read by JavaScript, so there is no
|
|
17
|
-
// session token sitting in `localStorage` for an XSS to lift. (Cross-origin or
|
|
18
|
-
// native clients, which can't rely on the cookie, use the token-based path via
|
|
19
|
-
// `persistSession` instead — not needed here, same origin.)
|
|
20
|
-
export function AuthForm({ mode }: { mode: "login" | "signup" }) {
|
|
21
|
-
const [email, setEmail] = useState("");
|
|
22
|
-
const [password, setPassword] = useState("");
|
|
23
|
-
const [displayName, setDisplayName] = useState("");
|
|
24
|
-
const [error, setError] = useState<string | null>(null);
|
|
25
|
-
const [pending, setPending] = useState(false);
|
|
26
|
-
|
|
27
|
-
async function onSubmit(e: React.FormEvent) {
|
|
28
|
-
e.preventDefault();
|
|
29
|
-
setError(null);
|
|
30
|
-
setPending(true);
|
|
31
|
-
try {
|
|
32
|
-
if (mode === "login") {
|
|
33
|
-
await passwordLogin({ email, password });
|
|
34
|
-
} else {
|
|
35
|
-
await passwordRegister({
|
|
36
|
-
email,
|
|
37
|
-
password,
|
|
38
|
-
displayName: displayName.trim() || undefined,
|
|
39
|
-
});
|
|
40
|
-
}
|
|
41
|
-
// Full navigation: the SSR dashboard re-renders with the new cookie.
|
|
42
|
-
window.location.assign("/dashboard");
|
|
43
|
-
} catch (err) {
|
|
44
|
-
setError(messageFor(err));
|
|
45
|
-
setPending(false); // keep the form up to retry (success navigates away)
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
return (
|
|
50
|
-
<form onSubmit={onSubmit} className="space-y-4">
|
|
51
|
-
{mode === "signup" ? (
|
|
52
|
-
<Field
|
|
53
|
-
label="Name"
|
|
54
|
-
value={displayName}
|
|
55
|
-
onChange={setDisplayName}
|
|
56
|
-
autoComplete="name"
|
|
57
|
-
placeholder="optional"
|
|
58
|
-
/>
|
|
59
|
-
) : null}
|
|
60
|
-
<Field
|
|
61
|
-
label="Email"
|
|
62
|
-
type="email"
|
|
63
|
-
value={email}
|
|
64
|
-
onChange={setEmail}
|
|
65
|
-
required
|
|
66
|
-
autoComplete="email"
|
|
67
|
-
placeholder="you@example.com"
|
|
68
|
-
/>
|
|
69
|
-
<Field
|
|
70
|
-
label="Password"
|
|
71
|
-
type="password"
|
|
72
|
-
value={password}
|
|
73
|
-
onChange={setPassword}
|
|
74
|
-
required
|
|
75
|
-
autoComplete={mode === "login" ? "current-password" : "new-password"}
|
|
76
|
-
placeholder={mode === "signup" ? "at least 8 characters" : undefined}
|
|
77
|
-
/>
|
|
78
|
-
{error ? (
|
|
79
|
-
<p className="rounded-md border border-red-600/30 bg-red-600/10 px-3 py-2 text-sm text-red-700">
|
|
80
|
-
{error}
|
|
81
|
-
</p>
|
|
82
|
-
) : null}
|
|
83
|
-
<Button type="submit" disabled={pending} className="w-full">
|
|
84
|
-
{pending ? "…" : mode === "login" ? "Sign in" : "Create account"}
|
|
85
|
-
</Button>
|
|
86
|
-
</form>
|
|
87
|
-
);
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
function Field({
|
|
91
|
-
label,
|
|
92
|
-
value,
|
|
93
|
-
onChange,
|
|
94
|
-
type = "text",
|
|
95
|
-
required,
|
|
96
|
-
autoComplete,
|
|
97
|
-
placeholder,
|
|
98
|
-
}: {
|
|
99
|
-
label: string;
|
|
100
|
-
value: string;
|
|
101
|
-
onChange: (v: string) => void;
|
|
102
|
-
type?: string;
|
|
103
|
-
required?: boolean;
|
|
104
|
-
autoComplete?: string;
|
|
105
|
-
placeholder?: string;
|
|
106
|
-
}) {
|
|
107
|
-
return (
|
|
108
|
-
<label className="block space-y-1.5">
|
|
109
|
-
<span className="text-sm font-medium">{label}</span>
|
|
110
|
-
<input
|
|
111
|
-
type={type}
|
|
112
|
-
value={value}
|
|
113
|
-
onChange={(e) => onChange(e.target.value)}
|
|
114
|
-
required={required}
|
|
115
|
-
autoComplete={autoComplete}
|
|
116
|
-
placeholder={placeholder}
|
|
117
|
-
className="flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-sm shadow-sm outline-none focus-visible:ring-1 focus-visible:ring-ring"
|
|
118
|
-
/>
|
|
119
|
-
</label>
|
|
120
|
-
);
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
// Map the framework's auth error codes to friendly copy. `ApiError` carries a
|
|
124
|
-
// stable `.code` (and `.status`) so you branch on the code, not the message.
|
|
125
|
-
function messageFor(err: unknown): string {
|
|
126
|
-
if (err instanceof ApiError) {
|
|
127
|
-
switch (err.code) {
|
|
128
|
-
case "INVALID_CREDENTIALS":
|
|
129
|
-
return "Wrong email or password.";
|
|
130
|
-
case "USER_EXISTS":
|
|
131
|
-
return "That email is already in use — sign in instead.";
|
|
132
|
-
case "WEAK_PASSWORD":
|
|
133
|
-
return "Pick a stronger password (at least 8 characters).";
|
|
134
|
-
case "RATE_LIMITED":
|
|
135
|
-
return "Too many attempts — try again in a minute.";
|
|
136
|
-
default:
|
|
137
|
-
return err.message;
|
|
138
|
-
}
|
|
139
|
-
}
|
|
140
|
-
if (err instanceof Error) return err.message;
|
|
141
|
-
return "Something went wrong. Try again.";
|
|
142
|
-
}
|
|
@@ -1,116 +0,0 @@
|
|
|
1
|
-
"use client";
|
|
2
|
-
|
|
3
|
-
import React, { useState } from "react";
|
|
4
|
-
import { db } from "@pylonsync/react";
|
|
5
|
-
import { useAuth } from "@pylonsync/client";
|
|
6
|
-
import { Button } from "@/components/ui/button";
|
|
7
|
-
|
|
8
|
-
export interface Note {
|
|
9
|
-
id: string;
|
|
10
|
-
body: string;
|
|
11
|
-
done: boolean;
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
// The interactive dashboard. `db.useQuery` is a LIVE subscription — it
|
|
15
|
-
// re-renders the instant a Note is added or toggled, in this tab or another.
|
|
16
|
-
// `db.insert` / `db.update` / `db.delete` are OPTIMISTIC: they apply to the
|
|
17
|
-
// local store immediately (zero-latency UI) and sync in the background,
|
|
18
|
-
// rolling back automatically if a policy rejects the write.
|
|
19
|
-
//
|
|
20
|
-
// `initial` are the rows the server rendered into the HTML (see page.tsx).
|
|
21
|
-
// We show them on the first paint — before the local store has hydrated — so
|
|
22
|
-
// there's no empty flash, then hand off to the live data. Server-rendered for
|
|
23
|
-
// the first byte, local-first realtime after.
|
|
24
|
-
export function Dashboard({ initial }: { initial: Note[] }) {
|
|
25
|
-
const { signOut } = useAuth();
|
|
26
|
-
const [body, setBody] = useState("");
|
|
27
|
-
const { data: live, loading } = db.useQuery<Note>("Note");
|
|
28
|
-
const notes = !loading || live.length > 0 ? live : initial;
|
|
29
|
-
|
|
30
|
-
async function addNote(e: React.FormEvent) {
|
|
31
|
-
e.preventDefault();
|
|
32
|
-
const text = body.trim();
|
|
33
|
-
if (!text) return;
|
|
34
|
-
setBody("");
|
|
35
|
-
// We don't send ownerId — `field.owner()` stamps it from the session
|
|
36
|
-
// server-side and rejects any forged value, so this optimistic insert is
|
|
37
|
-
// safe.
|
|
38
|
-
await db.insert("Note", { body: text, done: false });
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
async function onSignOut() {
|
|
42
|
-
// Clears the server session (DELETE /api/auth/session → the cookie is
|
|
43
|
-
// cleared), then we land back on the public homepage.
|
|
44
|
-
await signOut();
|
|
45
|
-
window.location.assign("/");
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
return (
|
|
49
|
-
<div className="space-y-5">
|
|
50
|
-
<div className="flex items-center justify-end">
|
|
51
|
-
<Button variant="ghost" size="sm" onClick={onSignOut}>
|
|
52
|
-
Sign out
|
|
53
|
-
</Button>
|
|
54
|
-
</div>
|
|
55
|
-
|
|
56
|
-
<form onSubmit={addNote} className="flex items-center gap-2">
|
|
57
|
-
<input
|
|
58
|
-
value={body}
|
|
59
|
-
onChange={(e) => setBody(e.target.value)}
|
|
60
|
-
placeholder="Write a note…"
|
|
61
|
-
aria-label="Note"
|
|
62
|
-
className="flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-sm shadow-sm outline-none focus-visible:ring-1 focus-visible:ring-ring"
|
|
63
|
-
/>
|
|
64
|
-
<Button type="submit">Add</Button>
|
|
65
|
-
</form>
|
|
66
|
-
|
|
67
|
-
{notes.length === 0 ? (
|
|
68
|
-
<p className="text-sm text-muted-foreground">
|
|
69
|
-
No notes yet — add one above. It appears instantly (optimistic) and
|
|
70
|
-
syncs; open this page in a second tab to watch it arrive live.
|
|
71
|
-
</p>
|
|
72
|
-
) : (
|
|
73
|
-
<ul className="space-y-2">
|
|
74
|
-
{notes.map((note) => (
|
|
75
|
-
<li
|
|
76
|
-
key={note.id}
|
|
77
|
-
className="flex items-center gap-3 rounded-md border px-3 py-2 text-sm"
|
|
78
|
-
>
|
|
79
|
-
<button
|
|
80
|
-
type="button"
|
|
81
|
-
aria-label={note.done ? "Mark not done" : "Mark done"}
|
|
82
|
-
onClick={() =>
|
|
83
|
-
db.update("Note", note.id, { done: !note.done })
|
|
84
|
-
}
|
|
85
|
-
className={
|
|
86
|
-
note.done
|
|
87
|
-
? "text-emerald-600"
|
|
88
|
-
: "text-muted-foreground/50 hover:text-muted-foreground"
|
|
89
|
-
}
|
|
90
|
-
>
|
|
91
|
-
{note.done ? "✓" : "○"}
|
|
92
|
-
</button>
|
|
93
|
-
<span
|
|
94
|
-
className={
|
|
95
|
-
note.done
|
|
96
|
-
? "flex-1 line-through text-muted-foreground"
|
|
97
|
-
: "flex-1"
|
|
98
|
-
}
|
|
99
|
-
>
|
|
100
|
-
{note.body}
|
|
101
|
-
</span>
|
|
102
|
-
<button
|
|
103
|
-
type="button"
|
|
104
|
-
aria-label="Delete note"
|
|
105
|
-
onClick={() => db.delete("Note", note.id)}
|
|
106
|
-
className="text-muted-foreground/40 hover:text-red-600"
|
|
107
|
-
>
|
|
108
|
-
✕
|
|
109
|
-
</button>
|
|
110
|
-
</li>
|
|
111
|
-
))}
|
|
112
|
-
</ul>
|
|
113
|
-
)}
|
|
114
|
-
</div>
|
|
115
|
-
);
|
|
116
|
-
}
|