@pylonsync/create-pylon 0.3.51 → 0.3.54

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 (180) hide show
  1. package/bin/create-pylon.js +347 -1156
  2. package/package.json +4 -3
  3. package/templates/_root/.env.example +9 -0
  4. package/templates/_root/README.md +43 -0
  5. package/templates/backend/b2b/apps/api/functions/archiveProject.ts +15 -0
  6. package/templates/backend/b2b/apps/api/functions/createOrg.ts +43 -0
  7. package/templates/backend/b2b/apps/api/functions/createProject.ts +25 -0
  8. package/templates/backend/b2b/apps/api/functions/inviteMember.ts +49 -0
  9. package/templates/backend/b2b/apps/api/functions/myOrgs.ts +37 -0
  10. package/templates/backend/b2b/apps/api/functions/orgMembers.ts +13 -0
  11. package/templates/backend/b2b/apps/api/functions/orgProjects.ts +18 -0
  12. package/templates/backend/b2b/apps/api/functions/removeMember.ts +29 -0
  13. package/templates/backend/b2b/apps/api/functions/setMemberRole.ts +38 -0
  14. package/templates/backend/b2b/apps/api/package.json +20 -0
  15. package/templates/backend/b2b/apps/api/schema.ts +171 -0
  16. package/templates/backend/b2b/apps/api/tsconfig.json +13 -0
  17. package/templates/backend/barebones/apps/api/functions/createWidget.ts +22 -0
  18. package/templates/backend/barebones/apps/api/functions/listWidgets.ts +18 -0
  19. package/templates/backend/barebones/apps/api/package.json +20 -0
  20. package/templates/backend/barebones/apps/api/schema.ts +61 -0
  21. package/templates/backend/barebones/apps/api/tsconfig.json +13 -0
  22. package/templates/backend/chat/apps/api/functions/createRoom.ts +32 -0
  23. package/templates/backend/chat/apps/api/functions/listRooms.ts +15 -0
  24. package/templates/backend/chat/apps/api/functions/roomMessages.ts +20 -0
  25. package/templates/backend/chat/apps/api/functions/sendMessage.ts +37 -0
  26. package/templates/backend/chat/apps/api/package.json +20 -0
  27. package/templates/backend/chat/apps/api/schema.ts +93 -0
  28. package/templates/backend/chat/apps/api/tsconfig.json +13 -0
  29. package/templates/backend/consumer/apps/api/functions/createPost.ts +48 -0
  30. package/templates/backend/consumer/apps/api/functions/deletePost.ts +21 -0
  31. package/templates/backend/consumer/apps/api/functions/feed.ts +57 -0
  32. package/templates/backend/consumer/apps/api/functions/myProfile.ts +17 -0
  33. package/templates/backend/consumer/apps/api/functions/profilePosts.ts +17 -0
  34. package/templates/backend/consumer/apps/api/functions/toggleLike.ts +48 -0
  35. package/templates/backend/consumer/apps/api/functions/upsertProfile.ts +70 -0
  36. package/templates/backend/consumer/apps/api/package.json +20 -0
  37. package/templates/backend/consumer/apps/api/schema.ts +130 -0
  38. package/templates/backend/consumer/apps/api/tsconfig.json +13 -0
  39. package/templates/backend/todo/apps/api/functions/addTodo.ts +27 -0
  40. package/templates/backend/todo/apps/api/functions/deleteTodo.ts +14 -0
  41. package/templates/backend/todo/apps/api/functions/editTodo.ts +16 -0
  42. package/templates/backend/todo/apps/api/functions/listTodos.ts +24 -0
  43. package/templates/backend/todo/apps/api/functions/reorderTodo.ts +14 -0
  44. package/templates/backend/todo/apps/api/functions/toggleTodo.ts +13 -0
  45. package/templates/backend/todo/apps/api/package.json +20 -0
  46. package/templates/backend/todo/apps/api/schema.ts +85 -0
  47. package/templates/backend/todo/apps/api/tsconfig.json +13 -0
  48. package/templates/expo/barebones/apps/expo/App.tsx +166 -0
  49. package/templates/expo/barebones/apps/expo/app.json +31 -0
  50. package/templates/expo/barebones/apps/expo/babel.config.js +6 -0
  51. package/templates/expo/barebones/apps/expo/package.json +30 -0
  52. package/templates/expo/barebones/apps/expo/tsconfig.json +16 -0
  53. package/templates/expo/chat/apps/expo/App.tsx +414 -0
  54. package/templates/expo/chat/apps/expo/app.json +25 -0
  55. package/templates/expo/chat/apps/expo/babel.config.js +6 -0
  56. package/templates/expo/chat/apps/expo/package.json +30 -0
  57. package/templates/expo/chat/apps/expo/tsconfig.json +16 -0
  58. package/templates/expo/consumer/apps/expo/App.tsx +360 -0
  59. package/templates/expo/consumer/apps/expo/app.json +25 -0
  60. package/templates/expo/consumer/apps/expo/babel.config.js +6 -0
  61. package/templates/expo/consumer/apps/expo/package.json +30 -0
  62. package/templates/expo/consumer/apps/expo/tsconfig.json +16 -0
  63. package/templates/expo/todo/apps/expo/App.tsx +287 -0
  64. package/templates/expo/todo/apps/expo/app.json +25 -0
  65. package/templates/expo/todo/apps/expo/babel.config.js +6 -0
  66. package/templates/expo/todo/apps/expo/package.json +30 -0
  67. package/templates/expo/todo/apps/expo/tsconfig.json +16 -0
  68. package/templates/ios/barebones/apps/ios/Package.swift +34 -0
  69. package/templates/ios/barebones/apps/ios/Sources/__APP_NAME_PASCAL__/ContentView.swift +98 -0
  70. package/templates/ios/barebones/apps/ios/Sources/__APP_NAME_PASCAL__/Models.swift +17 -0
  71. package/templates/ios/barebones/apps/ios/Sources/__APP_NAME_PASCAL__/__APP_NAME_PASCAL__App.swift +34 -0
  72. package/templates/ios/barebones/apps/ios/project.yml +42 -0
  73. package/templates/ios/chat/apps/ios/Package.swift +34 -0
  74. package/templates/ios/chat/apps/ios/Sources/__APP_NAME_PASCAL__/ChatRootView.swift +120 -0
  75. package/templates/ios/chat/apps/ios/Sources/__APP_NAME_PASCAL__/Models.swift +26 -0
  76. package/templates/ios/chat/apps/ios/Sources/__APP_NAME_PASCAL__/RoomView.swift +137 -0
  77. package/templates/ios/chat/apps/ios/Sources/__APP_NAME_PASCAL__/__APP_NAME_PASCAL__App.swift +35 -0
  78. package/templates/ios/chat/apps/ios/project.yml +42 -0
  79. package/templates/ios/consumer/apps/ios/Package.swift +23 -0
  80. package/templates/ios/consumer/apps/ios/Sources/__APP_NAME_PASCAL__/FeedView.swift +170 -0
  81. package/templates/ios/consumer/apps/ios/Sources/__APP_NAME_PASCAL__/Models.swift +42 -0
  82. package/templates/ios/consumer/apps/ios/Sources/__APP_NAME_PASCAL__/ProfileSetupView.swift +60 -0
  83. package/templates/ios/consumer/apps/ios/Sources/__APP_NAME_PASCAL__/RootView.swift +30 -0
  84. package/templates/ios/consumer/apps/ios/Sources/__APP_NAME_PASCAL__/__APP_NAME_PASCAL__App.swift +29 -0
  85. package/templates/ios/consumer/apps/ios/project.yml +42 -0
  86. package/templates/ios/todo/apps/ios/Package.swift +23 -0
  87. package/templates/ios/todo/apps/ios/Sources/__APP_NAME_PASCAL__/Models.swift +18 -0
  88. package/templates/ios/todo/apps/ios/Sources/__APP_NAME_PASCAL__/TodoListView.swift +230 -0
  89. package/templates/ios/todo/apps/ios/Sources/__APP_NAME_PASCAL__/__APP_NAME_PASCAL__App.swift +28 -0
  90. package/templates/ios/todo/apps/ios/project.yml +32 -0
  91. package/templates/mac/b2b/apps/mac/Package.swift +22 -0
  92. package/templates/mac/b2b/apps/mac/Sources/__APP_NAME_PASCAL__/Models.swift +15 -0
  93. package/templates/mac/b2b/apps/mac/Sources/__APP_NAME_PASCAL__/OrgPickerView.swift +178 -0
  94. package/templates/mac/b2b/apps/mac/Sources/__APP_NAME_PASCAL__/__APP_NAME_PASCAL__App.swift +30 -0
  95. package/templates/mac/b2b/apps/mac/__APP_NAME_PASCAL__.entitlements +13 -0
  96. package/templates/mac/b2b/apps/mac/project.yml +34 -0
  97. package/templates/mac/barebones/apps/mac/Package.swift +33 -0
  98. package/templates/mac/barebones/apps/mac/Sources/__APP_NAME_PASCAL__/ContentView.swift +104 -0
  99. package/templates/mac/barebones/apps/mac/Sources/__APP_NAME_PASCAL__/Models.swift +17 -0
  100. package/templates/mac/barebones/apps/mac/Sources/__APP_NAME_PASCAL__/__APP_NAME_PASCAL__App.swift +30 -0
  101. package/templates/mac/barebones/apps/mac/__APP_NAME_PASCAL__.entitlements +13 -0
  102. package/templates/mac/barebones/apps/mac/project.yml +34 -0
  103. package/templates/mac/chat/apps/mac/Package.swift +33 -0
  104. package/templates/mac/chat/apps/mac/Sources/__APP_NAME_PASCAL__/ChatRootView.swift +140 -0
  105. package/templates/mac/chat/apps/mac/Sources/__APP_NAME_PASCAL__/Models.swift +26 -0
  106. package/templates/mac/chat/apps/mac/Sources/__APP_NAME_PASCAL__/RoomView.swift +137 -0
  107. package/templates/mac/chat/apps/mac/Sources/__APP_NAME_PASCAL__/__APP_NAME_PASCAL__App.swift +37 -0
  108. package/templates/mac/chat/apps/mac/__APP_NAME_PASCAL__.entitlements +13 -0
  109. package/templates/mac/chat/apps/mac/project.yml +34 -0
  110. package/templates/mac/consumer/apps/mac/Package.swift +33 -0
  111. package/templates/mac/consumer/apps/mac/Sources/__APP_NAME_PASCAL__/FeedView.swift +170 -0
  112. package/templates/mac/consumer/apps/mac/Sources/__APP_NAME_PASCAL__/Models.swift +42 -0
  113. package/templates/mac/consumer/apps/mac/Sources/__APP_NAME_PASCAL__/ProfileSetupView.swift +60 -0
  114. package/templates/mac/consumer/apps/mac/Sources/__APP_NAME_PASCAL__/RootView.swift +30 -0
  115. package/templates/mac/consumer/apps/mac/Sources/__APP_NAME_PASCAL__/__APP_NAME_PASCAL__App.swift +31 -0
  116. package/templates/mac/consumer/apps/mac/__APP_NAME_PASCAL__.entitlements +13 -0
  117. package/templates/mac/consumer/apps/mac/project.yml +34 -0
  118. package/templates/mac/todo/apps/mac/Package.swift +33 -0
  119. package/templates/mac/todo/apps/mac/Sources/__APP_NAME_PASCAL__/Models.swift +19 -0
  120. package/templates/mac/todo/apps/mac/Sources/__APP_NAME_PASCAL__/TodoListView.swift +244 -0
  121. package/templates/mac/todo/apps/mac/Sources/__APP_NAME_PASCAL__/__APP_NAME_PASCAL__App.swift +30 -0
  122. package/templates/mac/todo/apps/mac/__APP_NAME_PASCAL__.entitlements +13 -0
  123. package/templates/mac/todo/apps/mac/project.yml +34 -0
  124. package/templates/ui/packages/ui/package.json +26 -0
  125. package/templates/ui/packages/ui/src/button.tsx +44 -0
  126. package/templates/ui/packages/ui/src/card.tsx +39 -0
  127. package/templates/ui/packages/ui/src/cn.ts +12 -0
  128. package/templates/ui/packages/ui/src/index.ts +4 -0
  129. package/templates/ui/packages/ui/src/input.tsx +19 -0
  130. package/templates/ui/packages/ui/tsconfig.json +15 -0
  131. package/templates/web/b2b/apps/web/next-env.d.ts +2 -0
  132. package/templates/web/b2b/apps/web/next.config.ts +24 -0
  133. package/templates/web/b2b/apps/web/package.json +29 -0
  134. package/templates/web/b2b/apps/web/postcss.config.mjs +3 -0
  135. package/templates/web/b2b/apps/web/src/app/components/OrgPicker.tsx +171 -0
  136. package/templates/web/b2b/apps/web/src/app/globals.css +6 -0
  137. package/templates/web/b2b/apps/web/src/app/layout.tsx +21 -0
  138. package/templates/web/b2b/apps/web/src/app/page.tsx +39 -0
  139. package/templates/web/b2b/apps/web/src/lib/pylon.ts +5 -0
  140. package/templates/web/b2b/apps/web/tsconfig.json +26 -0
  141. package/templates/web/barebones/apps/web/next-env.d.ts +2 -0
  142. package/templates/web/barebones/apps/web/next.config.ts +40 -0
  143. package/templates/web/barebones/apps/web/package.json +29 -0
  144. package/templates/web/barebones/apps/web/postcss.config.mjs +4 -0
  145. package/templates/web/barebones/apps/web/src/app/components/WidgetList.tsx +81 -0
  146. package/templates/web/barebones/apps/web/src/app/globals.css +9 -0
  147. package/templates/web/barebones/apps/web/src/app/layout.tsx +21 -0
  148. package/templates/web/barebones/apps/web/src/app/page.tsx +43 -0
  149. package/templates/web/barebones/apps/web/src/lib/pylon.ts +14 -0
  150. package/templates/web/barebones/apps/web/tsconfig.json +26 -0
  151. package/templates/web/chat/apps/web/next-env.d.ts +2 -0
  152. package/templates/web/chat/apps/web/next.config.ts +24 -0
  153. package/templates/web/chat/apps/web/package.json +29 -0
  154. package/templates/web/chat/apps/web/postcss.config.mjs +3 -0
  155. package/templates/web/chat/apps/web/src/app/components/ChatRoom.tsx +250 -0
  156. package/templates/web/chat/apps/web/src/app/globals.css +6 -0
  157. package/templates/web/chat/apps/web/src/app/layout.tsx +21 -0
  158. package/templates/web/chat/apps/web/src/app/page.tsx +51 -0
  159. package/templates/web/chat/apps/web/src/lib/pylon.ts +5 -0
  160. package/templates/web/chat/apps/web/tsconfig.json +26 -0
  161. package/templates/web/consumer/apps/web/next-env.d.ts +2 -0
  162. package/templates/web/consumer/apps/web/next.config.ts +24 -0
  163. package/templates/web/consumer/apps/web/package.json +29 -0
  164. package/templates/web/consumer/apps/web/postcss.config.mjs +3 -0
  165. package/templates/web/consumer/apps/web/src/app/components/Feed.tsx +295 -0
  166. package/templates/web/consumer/apps/web/src/app/globals.css +6 -0
  167. package/templates/web/consumer/apps/web/src/app/layout.tsx +21 -0
  168. package/templates/web/consumer/apps/web/src/app/page.tsx +55 -0
  169. package/templates/web/consumer/apps/web/src/lib/pylon.ts +5 -0
  170. package/templates/web/consumer/apps/web/tsconfig.json +26 -0
  171. package/templates/web/todo/apps/web/next-env.d.ts +2 -0
  172. package/templates/web/todo/apps/web/next.config.ts +24 -0
  173. package/templates/web/todo/apps/web/package.json +32 -0
  174. package/templates/web/todo/apps/web/postcss.config.mjs +3 -0
  175. package/templates/web/todo/apps/web/src/app/components/TodoList.tsx +310 -0
  176. package/templates/web/todo/apps/web/src/app/globals.css +6 -0
  177. package/templates/web/todo/apps/web/src/app/layout.tsx +21 -0
  178. package/templates/web/todo/apps/web/src/app/page.tsx +36 -0
  179. package/templates/web/todo/apps/web/src/lib/pylon.ts +5 -0
  180. package/templates/web/todo/apps/web/tsconfig.json +26 -0
@@ -0,0 +1,295 @@
1
+ "use client";
2
+
3
+ import { useState, useTransition } from "react";
4
+ import { Button, Input, Card, CardHeader, CardContent } from "@__APP_NAME_KEBAB__/ui";
5
+
6
+ type FeedItem = {
7
+ id: string;
8
+ body: string;
9
+ createdAt: string;
10
+ author: { id: string; handle: string; displayName: string } | null;
11
+ likeCount: number;
12
+ likedByMe: boolean;
13
+ };
14
+
15
+ type Profile = {
16
+ id: string;
17
+ userId: string;
18
+ handle: string;
19
+ displayName: string;
20
+ bio?: string | null;
21
+ createdAt: string;
22
+ };
23
+
24
+ export function Feed({
25
+ initialFeed,
26
+ initialMe,
27
+ }: {
28
+ initialFeed: FeedItem[];
29
+ initialMe: Profile | null;
30
+ }) {
31
+ const [feed, setFeed] = useState(initialFeed);
32
+ const [me, setMe] = useState(initialMe);
33
+ const [body, setBody] = useState("");
34
+ const [pending, startTransition] = useTransition();
35
+
36
+ if (!me) {
37
+ return <ProfileSetup onSaved={(p) => setMe(p)} />;
38
+ }
39
+
40
+ async function post() {
41
+ const trimmed = body.trim();
42
+ if (!trimmed) return;
43
+ setBody("");
44
+ startTransition(async () => {
45
+ const res = await fetch("/api/fn/createPost", {
46
+ method: "POST",
47
+ headers: { "Content-Type": "application/json" },
48
+ body: JSON.stringify({ body: trimmed }),
49
+ });
50
+ if (res.ok) {
51
+ const item = (await res.json()) as FeedItem;
52
+ setFeed((prev) => [item, ...prev]);
53
+ } else {
54
+ setBody(trimmed);
55
+ }
56
+ });
57
+ }
58
+
59
+ async function toggleLike(item: FeedItem) {
60
+ // Optimistic
61
+ setFeed((prev) =>
62
+ prev.map((p) =>
63
+ p.id === item.id
64
+ ? {
65
+ ...p,
66
+ likedByMe: !p.likedByMe,
67
+ likeCount: p.likeCount + (p.likedByMe ? -1 : 1),
68
+ }
69
+ : p,
70
+ ),
71
+ );
72
+ startTransition(async () => {
73
+ const res = await fetch("/api/fn/toggleLike", {
74
+ method: "POST",
75
+ headers: { "Content-Type": "application/json" },
76
+ body: JSON.stringify({ postId: item.id }),
77
+ });
78
+ if (!res.ok) {
79
+ // Revert
80
+ setFeed((prev) =>
81
+ prev.map((p) =>
82
+ p.id === item.id
83
+ ? {
84
+ ...p,
85
+ likedByMe: item.likedByMe,
86
+ likeCount: item.likeCount,
87
+ }
88
+ : p,
89
+ ),
90
+ );
91
+ }
92
+ });
93
+ }
94
+
95
+ async function remove(item: FeedItem) {
96
+ const snapshot = feed;
97
+ setFeed((prev) => prev.filter((p) => p.id !== item.id));
98
+ startTransition(async () => {
99
+ const res = await fetch("/api/fn/deletePost", {
100
+ method: "POST",
101
+ headers: { "Content-Type": "application/json" },
102
+ body: JSON.stringify({ id: item.id }),
103
+ });
104
+ if (!res.ok) setFeed(snapshot);
105
+ });
106
+ }
107
+
108
+ return (
109
+ <div className="space-y-6">
110
+ <Card>
111
+ <CardHeader>
112
+ <div className="flex items-center justify-between">
113
+ <div>
114
+ <div className="text-sm font-medium">{me.displayName}</div>
115
+ <div className="text-xs text-neutral-400 font-mono">
116
+ @{me.handle}
117
+ </div>
118
+ </div>
119
+ <button
120
+ className="text-xs text-neutral-500 hover:text-neutral-800 dark:hover:text-neutral-200"
121
+ onClick={() => setMe(null)}
122
+ >
123
+ Edit profile
124
+ </button>
125
+ </div>
126
+ </CardHeader>
127
+ <CardContent>
128
+ <form
129
+ onSubmit={(e) => {
130
+ e.preventDefault();
131
+ post();
132
+ }}
133
+ className="space-y-2"
134
+ >
135
+ <textarea
136
+ value={body}
137
+ onChange={(e) => setBody(e.target.value)}
138
+ placeholder={`What's on your mind, ${me.displayName.split(" ")[0]}?`}
139
+ rows={3}
140
+ maxLength={1000}
141
+ disabled={pending}
142
+ 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"
143
+ />
144
+ <div className="flex items-center justify-between">
145
+ <span className="text-xs text-neutral-400">
146
+ {body.length}/1000
147
+ </span>
148
+ <Button
149
+ type="submit"
150
+ variant="primary"
151
+ disabled={pending || !body.trim()}
152
+ size="sm"
153
+ >
154
+ Post
155
+ </Button>
156
+ </div>
157
+ </form>
158
+ </CardContent>
159
+ </Card>
160
+
161
+ {feed.length === 0 ? (
162
+ <p className="text-sm text-neutral-500 text-center py-8">
163
+ No posts yet. Be the first.
164
+ </p>
165
+ ) : (
166
+ <ul className="space-y-3">
167
+ {feed.map((item) => (
168
+ <li key={item.id}>
169
+ <Card>
170
+ <CardContent className="space-y-3">
171
+ <div className="flex items-baseline justify-between">
172
+ <div className="text-sm">
173
+ <span className="font-medium">
174
+ {item.author?.displayName ?? "Unknown"}
175
+ </span>{" "}
176
+ <span className="text-neutral-400 font-mono text-xs">
177
+ @{item.author?.handle ?? "?"}
178
+ </span>
179
+ </div>
180
+ <span className="text-xs text-neutral-400">
181
+ {new Date(item.createdAt).toLocaleString(undefined, {
182
+ month: "short",
183
+ day: "numeric",
184
+ hour: "numeric",
185
+ minute: "2-digit",
186
+ })}
187
+ </span>
188
+ </div>
189
+ <p className="text-sm whitespace-pre-wrap break-words">
190
+ {item.body}
191
+ </p>
192
+ <div className="flex items-center gap-2">
193
+ <button
194
+ onClick={() => toggleLike(item)}
195
+ className={`text-xs font-mono px-2 py-1 rounded border transition-colors ${
196
+ item.likedByMe
197
+ ? "border-pink-300 dark:border-pink-700 text-pink-600 dark:text-pink-300 bg-pink-50 dark:bg-pink-950"
198
+ : "border-neutral-200 dark:border-neutral-800 text-neutral-500 hover:bg-neutral-50 dark:hover:bg-neutral-900"
199
+ }`}
200
+ >
201
+ {item.likedByMe ? "♥" : "♡"} {item.likeCount}
202
+ </button>
203
+ {item.author?.id === me.id && (
204
+ <button
205
+ onClick={() => remove(item)}
206
+ className="text-xs text-neutral-400 hover:text-red-500"
207
+ >
208
+ Delete
209
+ </button>
210
+ )}
211
+ </div>
212
+ </CardContent>
213
+ </Card>
214
+ </li>
215
+ ))}
216
+ </ul>
217
+ )}
218
+ </div>
219
+ );
220
+ }
221
+
222
+ function ProfileSetup({ onSaved }: { onSaved: (p: Profile) => void }) {
223
+ const [handle, setHandle] = useState("");
224
+ const [displayName, setDisplayName] = useState("");
225
+ const [bio, setBio] = useState("");
226
+ const [pending, startTransition] = useTransition();
227
+ const [error, setError] = useState<string | null>(null);
228
+
229
+ async function save(e: React.FormEvent) {
230
+ e.preventDefault();
231
+ setError(null);
232
+ startTransition(async () => {
233
+ const res = await fetch("/api/fn/upsertProfile", {
234
+ method: "POST",
235
+ headers: { "Content-Type": "application/json" },
236
+ body: JSON.stringify({ handle, displayName, bio }),
237
+ });
238
+ if (res.ok) {
239
+ const profile = (await res.json()) as Profile;
240
+ onSaved(profile);
241
+ } else {
242
+ const body = await res.json().catch(() => ({}));
243
+ setError(body?.message ?? "save failed");
244
+ }
245
+ });
246
+ }
247
+
248
+ return (
249
+ <Card>
250
+ <CardHeader>
251
+ <h2 className="text-sm font-medium">Set up your profile</h2>
252
+ </CardHeader>
253
+ <CardContent>
254
+ <form onSubmit={save} className="space-y-3">
255
+ <div>
256
+ <label className="text-xs text-neutral-500 block mb-1">
257
+ Handle
258
+ </label>
259
+ <Input
260
+ value={handle}
261
+ onChange={(e) => setHandle(e.target.value.toLowerCase())}
262
+ placeholder="lowercase letters, digits, underscore"
263
+ pattern="[a-z0-9_]{2,20}"
264
+ required
265
+ />
266
+ </div>
267
+ <div>
268
+ <label className="text-xs text-neutral-500 block mb-1">
269
+ Display name
270
+ </label>
271
+ <Input
272
+ value={displayName}
273
+ onChange={(e) => setDisplayName(e.target.value)}
274
+ required
275
+ />
276
+ </div>
277
+ <div>
278
+ <label className="text-xs text-neutral-500 block mb-1">
279
+ Bio
280
+ </label>
281
+ <Input
282
+ value={bio}
283
+ onChange={(e) => setBio(e.target.value)}
284
+ placeholder="(optional)"
285
+ />
286
+ </div>
287
+ {error && <p className="text-xs text-red-500">{error}</p>}
288
+ <Button type="submit" variant="primary" disabled={pending}>
289
+ {pending ? "Saving…" : "Save"}
290
+ </Button>
291
+ </form>
292
+ </CardContent>
293
+ </Card>
294
+ );
295
+ }
@@ -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,21 @@
1
+ import type { Metadata } from "next";
2
+ import "./globals.css";
3
+
4
+ export const metadata: Metadata = {
5
+ title: "__APP_NAME__",
6
+ description: "Multi-tenant SaaS scaffold powered by Pylon",
7
+ };
8
+
9
+ export default function RootLayout({
10
+ children,
11
+ }: {
12
+ children: React.ReactNode;
13
+ }) {
14
+ return (
15
+ <html lang="en">
16
+ <body className="antialiased min-h-screen bg-white dark:bg-neutral-950 text-neutral-900 dark:text-neutral-100">
17
+ {children}
18
+ </body>
19
+ </html>
20
+ );
21
+ }
@@ -0,0 +1,55 @@
1
+ import { pylon } from "@/lib/pylon";
2
+ import { Feed } from "./components/Feed";
3
+
4
+ export const dynamic = "force-dynamic";
5
+
6
+ type FeedItem = {
7
+ id: string;
8
+ body: string;
9
+ createdAt: string;
10
+ author: { id: string; handle: string; displayName: string } | null;
11
+ likeCount: number;
12
+ likedByMe: boolean;
13
+ };
14
+
15
+ type Profile = {
16
+ id: string;
17
+ userId: string;
18
+ handle: string;
19
+ displayName: string;
20
+ bio?: string | null;
21
+ createdAt: string;
22
+ };
23
+
24
+ export default async function HomePage() {
25
+ const [feed, me] = await Promise.all([
26
+ pylon
27
+ .json<FeedItem[]>("/api/fn/feed", {
28
+ method: "POST",
29
+ body: "{}",
30
+ headers: { "Content-Type": "application/json" },
31
+ })
32
+ .catch(() => [] as FeedItem[]),
33
+ pylon
34
+ .json<Profile | null>("/api/fn/myProfile", {
35
+ method: "POST",
36
+ body: "{}",
37
+ headers: { "Content-Type": "application/json" },
38
+ })
39
+ .catch(() => null),
40
+ ]);
41
+
42
+ return (
43
+ <main className="mx-auto max-w-2xl px-6 py-12 space-y-8">
44
+ <header className="space-y-2">
45
+ <h1 className="text-3xl font-semibold tracking-tight">__APP_NAME__</h1>
46
+ <p className="text-sm text-neutral-500 dark:text-neutral-400">
47
+ Public feed scaffold. Profile, Post, Like — wide-open reads, owner-
48
+ only writes (enforced by Pylon row-level policies).
49
+ </p>
50
+ </header>
51
+
52
+ <Feed initialFeed={feed} initialMe={me} />
53
+ </main>
54
+ );
55
+ }
@@ -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
+ }
@@ -0,0 +1,2 @@
1
+ /// <reference types="next" />
2
+ /// <reference types="next/image-types/global" />
@@ -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,32 @@
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
+ "@dnd-kit/core": "^6.3.1",
18
+ "@dnd-kit/sortable": "^10.0.0",
19
+ "@dnd-kit/utilities": "^3.2.2",
20
+ "next": "^16.0.0",
21
+ "react": "^19.0.0",
22
+ "react-dom": "^19.0.0"
23
+ },
24
+ "devDependencies": {
25
+ "@types/node": "^20.0.0",
26
+ "@types/react": "^19.0.0",
27
+ "@types/react-dom": "^19.0.0",
28
+ "@tailwindcss/postcss": "^4.0.0",
29
+ "tailwindcss": "^4.0.0",
30
+ "typescript": "^5.5.0"
31
+ }
32
+ }
@@ -0,0 +1,3 @@
1
+ export default {
2
+ plugins: { "@tailwindcss/postcss": {} },
3
+ };