@pylonsync/create-pylon 0.3.53 → 0.3.55

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 (129) hide show
  1. package/bin/create-pylon.js +98 -42
  2. package/package.json +1 -1
  3. package/templates/backend/b2b/apps/api/functions/archiveProject.ts +15 -0
  4. package/templates/backend/b2b/apps/api/functions/createOrg.ts +43 -0
  5. package/templates/backend/b2b/apps/api/functions/createProject.ts +25 -0
  6. package/templates/backend/b2b/apps/api/functions/inviteMember.ts +49 -0
  7. package/templates/backend/b2b/apps/api/functions/myOrgs.ts +37 -0
  8. package/templates/backend/b2b/apps/api/functions/orgMembers.ts +13 -0
  9. package/templates/backend/b2b/apps/api/functions/orgProjects.ts +18 -0
  10. package/templates/backend/b2b/apps/api/functions/removeMember.ts +29 -0
  11. package/templates/backend/b2b/apps/api/functions/setMemberRole.ts +38 -0
  12. package/templates/backend/b2b/apps/api/package.json +20 -0
  13. package/templates/backend/b2b/apps/api/schema.ts +171 -0
  14. package/templates/backend/b2b/apps/api/tsconfig.json +13 -0
  15. package/templates/backend/chat/apps/api/functions/createRoom.ts +32 -0
  16. package/templates/backend/chat/apps/api/functions/listRooms.ts +15 -0
  17. package/templates/backend/chat/apps/api/functions/roomMessages.ts +20 -0
  18. package/templates/backend/chat/apps/api/functions/sendMessage.ts +37 -0
  19. package/templates/backend/chat/apps/api/package.json +20 -0
  20. package/templates/backend/chat/apps/api/schema.ts +93 -0
  21. package/templates/backend/chat/apps/api/tsconfig.json +13 -0
  22. package/templates/backend/consumer/apps/api/functions/createPost.ts +48 -0
  23. package/templates/backend/consumer/apps/api/functions/deletePost.ts +21 -0
  24. package/templates/backend/consumer/apps/api/functions/feed.ts +57 -0
  25. package/templates/backend/consumer/apps/api/functions/myProfile.ts +17 -0
  26. package/templates/backend/consumer/apps/api/functions/profilePosts.ts +17 -0
  27. package/templates/backend/consumer/apps/api/functions/toggleLike.ts +48 -0
  28. package/templates/backend/consumer/apps/api/functions/upsertProfile.ts +70 -0
  29. package/templates/backend/consumer/apps/api/package.json +20 -0
  30. package/templates/backend/consumer/apps/api/schema.ts +130 -0
  31. package/templates/backend/consumer/apps/api/tsconfig.json +13 -0
  32. package/templates/expo/chat/apps/expo/App.tsx +384 -0
  33. package/templates/expo/chat/apps/expo/app.json +25 -0
  34. package/templates/expo/chat/apps/expo/babel.config.js +6 -0
  35. package/templates/expo/chat/apps/expo/package.json +30 -0
  36. package/templates/expo/chat/apps/expo/tsconfig.json +16 -0
  37. package/templates/expo/consumer/apps/expo/App.tsx +392 -0
  38. package/templates/expo/consumer/apps/expo/app.json +25 -0
  39. package/templates/expo/consumer/apps/expo/babel.config.js +6 -0
  40. package/templates/expo/consumer/apps/expo/package.json +30 -0
  41. package/templates/expo/consumer/apps/expo/tsconfig.json +16 -0
  42. package/templates/ios/chat/apps/ios/Package.swift +24 -0
  43. package/templates/ios/chat/apps/ios/Sources/__APP_NAME_PASCAL__/ChatRootView.swift +116 -0
  44. package/templates/ios/chat/apps/ios/Sources/__APP_NAME_PASCAL__/Models.swift +26 -0
  45. package/templates/ios/chat/apps/ios/Sources/__APP_NAME_PASCAL__/RoomView.swift +136 -0
  46. package/templates/ios/chat/apps/ios/Sources/__APP_NAME_PASCAL__/__APP_NAME_PASCAL__App.swift +58 -0
  47. package/templates/ios/chat/apps/ios/project.yml +44 -0
  48. package/templates/ios/consumer/apps/ios/Package.swift +24 -0
  49. package/templates/ios/consumer/apps/ios/Sources/__APP_NAME_PASCAL__/FeedView.swift +199 -0
  50. package/templates/ios/consumer/apps/ios/Sources/__APP_NAME_PASCAL__/Models.swift +34 -0
  51. package/templates/ios/consumer/apps/ios/Sources/__APP_NAME_PASCAL__/ProfileSetupView.swift +68 -0
  52. package/templates/ios/consumer/apps/ios/Sources/__APP_NAME_PASCAL__/RootView.swift +40 -0
  53. package/templates/ios/consumer/apps/ios/Sources/__APP_NAME_PASCAL__/__APP_NAME_PASCAL__App.swift +57 -0
  54. package/templates/ios/consumer/apps/ios/project.yml +44 -0
  55. package/templates/mac/b2b/apps/mac/Package.swift +22 -0
  56. package/templates/mac/b2b/apps/mac/Sources/__APP_NAME_PASCAL__/Models.swift +15 -0
  57. package/templates/mac/b2b/apps/mac/Sources/__APP_NAME_PASCAL__/OrgPickerView.swift +178 -0
  58. package/templates/mac/b2b/apps/mac/Sources/__APP_NAME_PASCAL__/__APP_NAME_PASCAL__App.swift +30 -0
  59. package/templates/mac/b2b/apps/mac/__APP_NAME_PASCAL__.entitlements +13 -0
  60. package/templates/mac/b2b/apps/mac/project.yml +34 -0
  61. package/templates/mac/barebones/apps/mac/Package.swift +33 -0
  62. package/templates/mac/barebones/apps/mac/Sources/__APP_NAME_PASCAL__/ContentView.swift +104 -0
  63. package/templates/mac/barebones/apps/mac/Sources/__APP_NAME_PASCAL__/Models.swift +17 -0
  64. package/templates/mac/barebones/apps/mac/Sources/__APP_NAME_PASCAL__/__APP_NAME_PASCAL__App.swift +30 -0
  65. package/templates/mac/barebones/apps/mac/__APP_NAME_PASCAL__.entitlements +13 -0
  66. package/templates/mac/barebones/apps/mac/project.yml +34 -0
  67. package/templates/mac/chat/apps/mac/Package.swift +34 -0
  68. package/templates/mac/chat/apps/mac/Sources/__APP_NAME_PASCAL__/ChatRootView.swift +143 -0
  69. package/templates/mac/chat/apps/mac/Sources/__APP_NAME_PASCAL__/Models.swift +26 -0
  70. package/templates/mac/chat/apps/mac/Sources/__APP_NAME_PASCAL__/RoomView.swift +136 -0
  71. package/templates/mac/chat/apps/mac/Sources/__APP_NAME_PASCAL__/__APP_NAME_PASCAL__App.swift +58 -0
  72. package/templates/mac/chat/apps/mac/__APP_NAME_PASCAL__.entitlements +13 -0
  73. package/templates/mac/chat/apps/mac/project.yml +36 -0
  74. package/templates/mac/consumer/apps/mac/Package.swift +34 -0
  75. package/templates/mac/consumer/apps/mac/Sources/__APP_NAME_PASCAL__/FeedView.swift +199 -0
  76. package/templates/mac/consumer/apps/mac/Sources/__APP_NAME_PASCAL__/Models.swift +34 -0
  77. package/templates/mac/consumer/apps/mac/Sources/__APP_NAME_PASCAL__/ProfileSetupView.swift +68 -0
  78. package/templates/mac/consumer/apps/mac/Sources/__APP_NAME_PASCAL__/RootView.swift +40 -0
  79. package/templates/mac/consumer/apps/mac/Sources/__APP_NAME_PASCAL__/__APP_NAME_PASCAL__App.swift +59 -0
  80. package/templates/mac/consumer/apps/mac/__APP_NAME_PASCAL__.entitlements +13 -0
  81. package/templates/mac/consumer/apps/mac/project.yml +36 -0
  82. package/templates/mac/todo/apps/mac/Package.swift +33 -0
  83. package/templates/mac/todo/apps/mac/Sources/__APP_NAME_PASCAL__/Models.swift +19 -0
  84. package/templates/mac/todo/apps/mac/Sources/__APP_NAME_PASCAL__/TodoListView.swift +244 -0
  85. package/templates/mac/todo/apps/mac/Sources/__APP_NAME_PASCAL__/__APP_NAME_PASCAL__App.swift +30 -0
  86. package/templates/mac/todo/apps/mac/__APP_NAME_PASCAL__.entitlements +13 -0
  87. package/templates/mac/todo/apps/mac/project.yml +34 -0
  88. package/templates/web/b2b/apps/web/next-env.d.ts +2 -0
  89. package/templates/web/b2b/apps/web/next.config.ts +24 -0
  90. package/templates/web/b2b/apps/web/package.json +29 -0
  91. package/templates/web/b2b/apps/web/postcss.config.mjs +3 -0
  92. package/templates/web/b2b/apps/web/src/app/components/OrgPicker.tsx +171 -0
  93. package/templates/web/b2b/apps/web/src/app/globals.css +6 -0
  94. package/templates/web/b2b/apps/web/src/app/layout.tsx +21 -0
  95. package/templates/web/b2b/apps/web/src/app/page.tsx +39 -0
  96. package/templates/web/b2b/apps/web/src/lib/pylon.ts +5 -0
  97. package/templates/web/b2b/apps/web/tsconfig.json +26 -0
  98. package/templates/web/chat/apps/web/next-env.d.ts +2 -0
  99. package/templates/web/chat/apps/web/next.config.ts +24 -0
  100. package/templates/web/chat/apps/web/package.json +29 -0
  101. package/templates/web/chat/apps/web/postcss.config.mjs +3 -0
  102. package/templates/web/chat/apps/web/src/app/components/ChatRoom.tsx +231 -0
  103. package/templates/web/chat/apps/web/src/app/globals.css +6 -0
  104. package/templates/web/chat/apps/web/src/app/layout.tsx +22 -0
  105. package/templates/web/chat/apps/web/src/app/page.tsx +12 -0
  106. package/templates/web/chat/apps/web/src/app/providers.tsx +25 -0
  107. package/templates/web/chat/apps/web/src/lib/pylon.ts +5 -0
  108. package/templates/web/chat/apps/web/tsconfig.json +26 -0
  109. package/templates/web/consumer/apps/web/next-env.d.ts +2 -0
  110. package/templates/web/consumer/apps/web/next.config.ts +24 -0
  111. package/templates/web/consumer/apps/web/package.json +29 -0
  112. package/templates/web/consumer/apps/web/postcss.config.mjs +3 -0
  113. package/templates/web/consumer/apps/web/src/app/components/Feed.tsx +315 -0
  114. package/templates/web/consumer/apps/web/src/app/globals.css +6 -0
  115. package/templates/web/consumer/apps/web/src/app/layout.tsx +22 -0
  116. package/templates/web/consumer/apps/web/src/app/page.tsx +21 -0
  117. package/templates/web/consumer/apps/web/src/app/providers.tsx +25 -0
  118. package/templates/web/consumer/apps/web/src/lib/pylon.ts +5 -0
  119. package/templates/web/consumer/apps/web/tsconfig.json +26 -0
  120. /package/templates/{mobile/barebones/apps/mobile → ios/barebones/apps/ios}/Package.swift +0 -0
  121. /package/templates/{mobile/barebones/apps/mobile → ios/barebones/apps/ios}/Sources/__APP_NAME_PASCAL__/ContentView.swift +0 -0
  122. /package/templates/{mobile/barebones/apps/mobile → ios/barebones/apps/ios}/Sources/__APP_NAME_PASCAL__/Models.swift +0 -0
  123. /package/templates/{mobile/barebones/apps/mobile → ios/barebones/apps/ios}/Sources/__APP_NAME_PASCAL__/__APP_NAME_PASCAL__App.swift +0 -0
  124. /package/templates/{mobile/barebones/apps/mobile → ios/barebones/apps/ios}/project.yml +0 -0
  125. /package/templates/{mobile/todo/apps/mobile → ios/todo/apps/ios}/Package.swift +0 -0
  126. /package/templates/{mobile/todo/apps/mobile → ios/todo/apps/ios}/Sources/__APP_NAME_PASCAL__/Models.swift +0 -0
  127. /package/templates/{mobile/todo/apps/mobile → ios/todo/apps/ios}/Sources/__APP_NAME_PASCAL__/TodoListView.swift +0 -0
  128. /package/templates/{mobile/todo/apps/mobile → ios/todo/apps/ios}/Sources/__APP_NAME_PASCAL__/__APP_NAME_PASCAL__App.swift +0 -0
  129. /package/templates/{mobile/todo/apps/mobile → ios/todo/apps/ios}/project.yml +0 -0
@@ -0,0 +1,24 @@
1
+ import type { NextConfig } from "next";
2
+
3
+ const PYLON_API_URL = process.env.PYLON_API_URL ?? "http://localhost:4321";
4
+
5
+ const config: NextConfig = {
6
+ transpilePackages: [
7
+ "@__APP_NAME_KEBAB__/ui",
8
+ "@pylonsync/sdk",
9
+ "@pylonsync/react",
10
+ "@pylonsync/next",
11
+ "@pylonsync/functions",
12
+ "@pylonsync/sync",
13
+ ],
14
+ async rewrites() {
15
+ return [
16
+ { source: "/api/fn/:path*", destination: `${PYLON_API_URL}/api/fn/:path*` },
17
+ { source: "/api/auth/:path*", destination: `${PYLON_API_URL}/api/auth/:path*` },
18
+ { source: "/api/sync/:path*", destination: `${PYLON_API_URL}/api/sync/:path*` },
19
+ { source: "/api/:path*", destination: `${PYLON_API_URL}/api/:path*` },
20
+ ];
21
+ },
22
+ };
23
+
24
+ export default config;
@@ -0,0 +1,29 @@
1
+ {
2
+ "name": "@__APP_NAME_KEBAB__/web",
3
+ "version": "0.0.1",
4
+ "private": true,
5
+ "type": "module",
6
+ "scripts": {
7
+ "dev": "next dev --port 3000",
8
+ "build": "next build",
9
+ "start": "next start",
10
+ "lint": "next lint"
11
+ },
12
+ "dependencies": {
13
+ "@__APP_NAME_KEBAB__/ui": "__WORKSPACE_DEP__",
14
+ "@pylonsync/sdk": "^__PYLON_VERSION__",
15
+ "@pylonsync/react": "^__PYLON_VERSION__",
16
+ "@pylonsync/next": "^__PYLON_VERSION__",
17
+ "next": "^16.0.0",
18
+ "react": "^19.0.0",
19
+ "react-dom": "^19.0.0"
20
+ },
21
+ "devDependencies": {
22
+ "@types/node": "^20.0.0",
23
+ "@types/react": "^19.0.0",
24
+ "@types/react-dom": "^19.0.0",
25
+ "@tailwindcss/postcss": "^4.0.0",
26
+ "tailwindcss": "^4.0.0",
27
+ "typescript": "^5.5.0"
28
+ }
29
+ }
@@ -0,0 +1,3 @@
1
+ export default {
2
+ plugins: { "@tailwindcss/postcss": {} },
3
+ };
@@ -0,0 +1,315 @@
1
+ "use client";
2
+
3
+ import { useMemo, useState } from "react";
4
+ import { db, callFn } from "@pylonsync/react";
5
+ import { Button, Input, Card, CardHeader, CardContent } from "@__APP_NAME_KEBAB__/ui";
6
+
7
+ type Post = {
8
+ id: string;
9
+ authorId: string;
10
+ body: string;
11
+ createdAt: string;
12
+ };
13
+
14
+ type Profile = {
15
+ id: string;
16
+ userId: string;
17
+ handle: string;
18
+ displayName: string;
19
+ bio?: string | null;
20
+ createdAt: string;
21
+ };
22
+
23
+ type Like = {
24
+ id: string;
25
+ postId: string;
26
+ profileId: string;
27
+ createdAt: string;
28
+ };
29
+
30
+ export function Feed() {
31
+ // Three live subscriptions. The sync engine pushes diffs over
32
+ // WebSocket so a new Post / Like from any other client renders
33
+ // here within ~ms. Joining client-side keeps the schema simple
34
+ // — for very large feeds, a server-side aggregated query is
35
+ // cheaper, but the live story is more important to demonstrate.
36
+ const { data: posts = [] } = db.useQuery<Post>("Post", {
37
+ orderBy: { createdAt: "desc" },
38
+ limit: 100,
39
+ });
40
+ const { data: profiles = [] } = db.useQuery<Profile>("Profile", {});
41
+ const { data: likes = [] } = db.useQuery<Like>("Like", {});
42
+ const { data: myProfileRows = [] } = db.useQuery<Profile>("Profile", {});
43
+
44
+ const profilesById = useMemo(() => {
45
+ const map = new Map<string, Profile>();
46
+ for (const p of profiles) map.set(p.id, p);
47
+ return map;
48
+ }, [profiles]);
49
+
50
+ const myProfile = useMemo(() => {
51
+ // `myProfile` server-side filters by auth.userId; from the
52
+ // client we don't have userId here without a separate auth
53
+ // fetch, so we lean on the Profile policy: every Profile
54
+ // row in the local store IS visible (public reads). To find
55
+ // "ours" we'd want auth.userId; the scaffold uses a
56
+ // localStorage-cached selection instead.
57
+ if (typeof window === "undefined") return null;
58
+ const id = window.localStorage.getItem("__APP_NAME_SNAKE___profile_id");
59
+ if (!id) return null;
60
+ return myProfileRows.find((p) => p.id === id) ?? null;
61
+ }, [myProfileRows]);
62
+
63
+ if (!myProfile) {
64
+ return (
65
+ <ProfileSetup
66
+ existingHandles={profiles.map((p) => p.handle)}
67
+ onSaved={(p) => {
68
+ window.localStorage.setItem("__APP_NAME_SNAKE___profile_id", p.id);
69
+ // Force re-render by reading from the live profiles array.
70
+ }}
71
+ />
72
+ );
73
+ }
74
+
75
+ const items = posts.map((post) => {
76
+ const author = profilesById.get(post.authorId) ?? null;
77
+ const postLikes = likes.filter((l) => l.postId === post.id);
78
+ const likedByMe = postLikes.some((l) => l.profileId === myProfile.id);
79
+ return { post, author, likeCount: postLikes.length, likedByMe };
80
+ });
81
+
82
+ return <FeedView me={myProfile} items={items} />;
83
+ }
84
+
85
+ function FeedView({
86
+ me,
87
+ items,
88
+ }: {
89
+ me: Profile;
90
+ items: Array<{
91
+ post: Post;
92
+ author: Profile | null;
93
+ likeCount: number;
94
+ likedByMe: boolean;
95
+ }>;
96
+ }) {
97
+ const [body, setBody] = useState("");
98
+ const [posting, setPosting] = useState(false);
99
+
100
+ async function post() {
101
+ const trimmed = body.trim();
102
+ if (!trimmed) return;
103
+ setBody("");
104
+ setPosting(true);
105
+ try {
106
+ await callFn("createPost", { body: trimmed });
107
+ } finally {
108
+ setPosting(false);
109
+ }
110
+ }
111
+
112
+ async function toggleLike(postId: string) {
113
+ try {
114
+ await callFn("toggleLike", { postId });
115
+ } catch (e) {
116
+ window.alert(`Like failed: ${String(e)}`);
117
+ }
118
+ }
119
+
120
+ async function remove(postId: string) {
121
+ try {
122
+ await callFn("deletePost", { id: postId });
123
+ } catch (e) {
124
+ window.alert(`Delete failed: ${String(e)}`);
125
+ }
126
+ }
127
+
128
+ return (
129
+ <div className="space-y-6">
130
+ <Card>
131
+ <CardHeader>
132
+ <div className="flex items-center justify-between">
133
+ <div>
134
+ <div className="text-sm font-medium">{me.displayName}</div>
135
+ <div className="text-xs text-neutral-400 font-mono">
136
+ @{me.handle}
137
+ </div>
138
+ </div>
139
+ </div>
140
+ </CardHeader>
141
+ <CardContent>
142
+ <form
143
+ onSubmit={(e) => {
144
+ e.preventDefault();
145
+ post();
146
+ }}
147
+ className="space-y-2"
148
+ >
149
+ <textarea
150
+ value={body}
151
+ onChange={(e) => setBody(e.target.value)}
152
+ placeholder={`What's on your mind, ${me.displayName.split(" ")[0]}?`}
153
+ rows={3}
154
+ maxLength={1000}
155
+ disabled={posting}
156
+ className="w-full rounded-md border border-neutral-300 dark:border-neutral-700 bg-white dark:bg-neutral-900 px-3 py-2 text-sm placeholder:text-neutral-400 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 resize-none"
157
+ />
158
+ <div className="flex items-center justify-between">
159
+ <span className="text-xs text-neutral-400">
160
+ {body.length}/1000
161
+ </span>
162
+ <Button
163
+ type="submit"
164
+ variant="primary"
165
+ disabled={posting || !body.trim()}
166
+ size="sm"
167
+ >
168
+ Post
169
+ </Button>
170
+ </div>
171
+ </form>
172
+ </CardContent>
173
+ </Card>
174
+
175
+ {items.length === 0 ? (
176
+ <p className="text-sm text-neutral-500 text-center py-8">
177
+ No posts yet. Be the first.
178
+ </p>
179
+ ) : (
180
+ <ul className="space-y-3">
181
+ {items.map(({ post, author, likeCount, likedByMe }) => (
182
+ <li key={post.id}>
183
+ <Card>
184
+ <CardContent className="space-y-3">
185
+ <div className="flex items-baseline justify-between">
186
+ <div className="text-sm">
187
+ <span className="font-medium">
188
+ {author?.displayName ?? "Unknown"}
189
+ </span>{" "}
190
+ <span className="text-neutral-400 font-mono text-xs">
191
+ @{author?.handle ?? "?"}
192
+ </span>
193
+ </div>
194
+ <span className="text-xs text-neutral-400">
195
+ {new Date(post.createdAt).toLocaleString(undefined, {
196
+ month: "short",
197
+ day: "numeric",
198
+ hour: "numeric",
199
+ minute: "2-digit",
200
+ })}
201
+ </span>
202
+ </div>
203
+ <p className="text-sm whitespace-pre-wrap break-words">
204
+ {post.body}
205
+ </p>
206
+ <div className="flex items-center gap-2">
207
+ <button
208
+ onClick={() => toggleLike(post.id)}
209
+ className={`text-xs font-mono px-2 py-1 rounded border transition-colors ${
210
+ likedByMe
211
+ ? "border-pink-300 dark:border-pink-700 text-pink-600 dark:text-pink-300 bg-pink-50 dark:bg-pink-950"
212
+ : "border-neutral-200 dark:border-neutral-800 text-neutral-500 hover:bg-neutral-50 dark:hover:bg-neutral-900"
213
+ }`}
214
+ >
215
+ {likedByMe ? "♥" : "♡"} {likeCount}
216
+ </button>
217
+ {author?.id === me.id && (
218
+ <button
219
+ onClick={() => remove(post.id)}
220
+ className="text-xs text-neutral-400 hover:text-red-500"
221
+ >
222
+ Delete
223
+ </button>
224
+ )}
225
+ </div>
226
+ </CardContent>
227
+ </Card>
228
+ </li>
229
+ ))}
230
+ </ul>
231
+ )}
232
+ </div>
233
+ );
234
+ }
235
+
236
+ function ProfileSetup({
237
+ existingHandles,
238
+ onSaved,
239
+ }: {
240
+ existingHandles: string[];
241
+ onSaved: (p: Profile) => void;
242
+ }) {
243
+ const [handle, setHandle] = useState("");
244
+ const [displayName, setDisplayName] = useState("");
245
+ const [bio, setBio] = useState("");
246
+ const [saving, setSaving] = useState(false);
247
+ const [error, setError] = useState<string | null>(null);
248
+
249
+ async function save(e: React.FormEvent) {
250
+ e.preventDefault();
251
+ setError(null);
252
+ const lower = handle.toLowerCase();
253
+ if (existingHandles.includes(lower)) {
254
+ setError(`@${lower} is taken`);
255
+ return;
256
+ }
257
+ setSaving(true);
258
+ try {
259
+ const profile = (await callFn("upsertProfile", {
260
+ handle: lower,
261
+ displayName,
262
+ bio,
263
+ })) as Profile;
264
+ onSaved(profile);
265
+ } catch (e) {
266
+ setError(String(e));
267
+ } finally {
268
+ setSaving(false);
269
+ }
270
+ }
271
+
272
+ return (
273
+ <Card>
274
+ <CardHeader>
275
+ <h2 className="text-sm font-medium">Set up your profile</h2>
276
+ </CardHeader>
277
+ <CardContent>
278
+ <form onSubmit={save} className="space-y-3">
279
+ <div>
280
+ <label className="text-xs text-neutral-500 block mb-1">Handle</label>
281
+ <Input
282
+ value={handle}
283
+ onChange={(e) => setHandle(e.target.value.toLowerCase())}
284
+ placeholder="lowercase letters, digits, underscore"
285
+ pattern="[a-z0-9_]{2,20}"
286
+ required
287
+ />
288
+ </div>
289
+ <div>
290
+ <label className="text-xs text-neutral-500 block mb-1">
291
+ Display name
292
+ </label>
293
+ <Input
294
+ value={displayName}
295
+ onChange={(e) => setDisplayName(e.target.value)}
296
+ required
297
+ />
298
+ </div>
299
+ <div>
300
+ <label className="text-xs text-neutral-500 block mb-1">Bio</label>
301
+ <Input
302
+ value={bio}
303
+ onChange={(e) => setBio(e.target.value)}
304
+ placeholder="(optional)"
305
+ />
306
+ </div>
307
+ {error && <p className="text-xs text-red-500">{error}</p>}
308
+ <Button type="submit" variant="primary" disabled={saving}>
309
+ {saving ? "Saving…" : "Save"}
310
+ </Button>
311
+ </form>
312
+ </CardContent>
313
+ </Card>
314
+ );
315
+ }
@@ -0,0 +1,6 @@
1
+ @import "tailwindcss";
2
+ @source "../../../../packages/ui/src/**/*.{ts,tsx}";
3
+
4
+ :root { color-scheme: light dark; }
5
+ html, body { height: 100%; }
6
+ body { font-family: ui-sans-serif, system-ui, -apple-system, sans-serif; }
@@ -0,0 +1,22 @@
1
+ import type { Metadata } from "next";
2
+ import "./globals.css";
3
+ import { Providers } from "./providers";
4
+
5
+ export const metadata: Metadata = {
6
+ title: "__APP_NAME__",
7
+ description: "Realtime social feed powered by Pylon",
8
+ };
9
+
10
+ export default function RootLayout({
11
+ children,
12
+ }: {
13
+ children: React.ReactNode;
14
+ }) {
15
+ return (
16
+ <html lang="en">
17
+ <body className="antialiased min-h-screen bg-white dark:bg-neutral-950 text-neutral-900 dark:text-neutral-100">
18
+ <Providers>{children}</Providers>
19
+ </body>
20
+ </html>
21
+ );
22
+ }
@@ -0,0 +1,21 @@
1
+ import { Feed } from "./components/Feed";
2
+
3
+ // Feed subscribes to Post / Profile / Like via db.useQuery — every
4
+ // new post + every like from any other client re-renders without
5
+ // a fetch. No server-side initial data; the sync engine hydrates
6
+ // on first connect.
7
+ export default function HomePage() {
8
+ return (
9
+ <main className="mx-auto max-w-2xl px-6 py-12 space-y-8">
10
+ <header className="space-y-2">
11
+ <h1 className="text-3xl font-semibold tracking-tight">__APP_NAME__</h1>
12
+ <p className="text-sm text-neutral-500 dark:text-neutral-400">
13
+ Public feed. Profile / Post / Like — wide-open reads, owner-only
14
+ writes. Live updates via Pylon&apos;s sync engine.
15
+ </p>
16
+ </header>
17
+
18
+ <Feed />
19
+ </main>
20
+ );
21
+ }
@@ -0,0 +1,25 @@
1
+ "use client";
2
+
3
+ import { useEffect, useState } from "react";
4
+ import { init } from "@pylonsync/react";
5
+
6
+ /**
7
+ * Boots the Pylon sync engine on first mount. Every `db.useQuery`
8
+ * downstream renders against the same engine instance, so a single
9
+ * top-level Providers wrapper is enough — no need to thread a client
10
+ * through context.
11
+ *
12
+ * `baseUrl` is the same-origin Next path because next.config.ts
13
+ * proxies /api/* to the Pylon binary on PYLON_API_URL. Browser →
14
+ * same-origin → Next server → Pylon: no CORS, no extra DNS, the
15
+ * cookie travels naturally.
16
+ */
17
+ export function Providers({ children }: { children: React.ReactNode }) {
18
+ const [ready, setReady] = useState(false);
19
+ useEffect(() => {
20
+ init({ baseUrl: "", appName: "__APP_NAME_SNAKE__" });
21
+ setReady(true);
22
+ }, []);
23
+ if (!ready) return null;
24
+ return <>{children}</>;
25
+ }
@@ -0,0 +1,5 @@
1
+ import { createPylonServer } from "@pylonsync/next/server";
2
+
3
+ export const pylon = createPylonServer({
4
+ cookieName: "__APP_NAME_SNAKE___session",
5
+ });
@@ -0,0 +1,26 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "lib": ["dom", "dom.iterable", "esnext"],
5
+ "allowJs": true,
6
+ "skipLibCheck": true,
7
+ "strict": true,
8
+ "noEmit": true,
9
+ "esModuleInterop": true,
10
+ "module": "esnext",
11
+ "moduleResolution": "bundler",
12
+ "resolveJsonModule": true,
13
+ "isolatedModules": true,
14
+ "jsx": "preserve",
15
+ "incremental": true,
16
+ "plugins": [{ "name": "next" }],
17
+ "paths": { "@/*": ["./src/*"] }
18
+ },
19
+ "include": [
20
+ "next-env.d.ts",
21
+ "src/**/*.ts",
22
+ "src/**/*.tsx",
23
+ ".next/types/**/*.ts"
24
+ ],
25
+ "exclude": ["node_modules"]
26
+ }