@pylonsync/create-pylon 0.3.54 → 0.3.57
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 +28 -51
- package/package.json +1 -1
- package/templates/_root/turbo.json +18 -0
- package/templates/backend/b2b/apps/api/package.json +3 -3
- package/templates/backend/barebones/apps/api/package.json +3 -3
- package/templates/backend/chat/apps/api/package.json +3 -3
- package/templates/backend/consumer/apps/api/package.json +3 -3
- package/templates/backend/todo/apps/api/package.json +3 -3
- package/templates/expo/barebones/apps/expo/package.json +1 -0
- package/templates/expo/chat/apps/expo/App.tsx +126 -156
- package/templates/expo/chat/apps/expo/package.json +1 -0
- package/templates/expo/consumer/apps/expo/App.tsx +211 -179
- package/templates/expo/consumer/apps/expo/package.json +1 -0
- package/templates/expo/todo/apps/expo/package.json +1 -0
- package/templates/ios/chat/apps/ios/Package.swift +1 -11
- package/templates/ios/chat/apps/ios/Sources/__APP_NAME_PASCAL__/ChatRootView.swift +26 -30
- package/templates/ios/chat/apps/ios/Sources/__APP_NAME_PASCAL__/RoomView.swift +35 -36
- package/templates/ios/chat/apps/ios/Sources/__APP_NAME_PASCAL__/__APP_NAME_PASCAL__App.swift +27 -4
- package/templates/ios/chat/apps/ios/project.yml +2 -0
- package/templates/ios/consumer/apps/ios/Package.swift +1 -0
- package/templates/ios/consumer/apps/ios/Sources/__APP_NAME_PASCAL__/FeedView.swift +81 -52
- package/templates/ios/consumer/apps/ios/Sources/__APP_NAME_PASCAL__/Models.swift +6 -14
- package/templates/ios/consumer/apps/ios/Sources/__APP_NAME_PASCAL__/ProfileSetupView.swift +14 -6
- package/templates/ios/consumer/apps/ios/Sources/__APP_NAME_PASCAL__/RootView.swift +25 -15
- package/templates/ios/consumer/apps/ios/Sources/__APP_NAME_PASCAL__/__APP_NAME_PASCAL__App.swift +33 -5
- package/templates/ios/consumer/apps/ios/project.yml +2 -0
- package/templates/mac/chat/apps/mac/Package.swift +1 -0
- package/templates/mac/chat/apps/mac/Sources/__APP_NAME_PASCAL__/ChatRootView.swift +30 -27
- package/templates/mac/chat/apps/mac/Sources/__APP_NAME_PASCAL__/RoomView.swift +35 -36
- package/templates/mac/chat/apps/mac/Sources/__APP_NAME_PASCAL__/__APP_NAME_PASCAL__App.swift +26 -5
- package/templates/mac/chat/apps/mac/project.yml +2 -0
- package/templates/mac/consumer/apps/mac/Package.swift +1 -0
- package/templates/mac/consumer/apps/mac/Sources/__APP_NAME_PASCAL__/FeedView.swift +81 -52
- package/templates/mac/consumer/apps/mac/Sources/__APP_NAME_PASCAL__/Models.swift +6 -14
- package/templates/mac/consumer/apps/mac/Sources/__APP_NAME_PASCAL__/ProfileSetupView.swift +14 -6
- package/templates/mac/consumer/apps/mac/Sources/__APP_NAME_PASCAL__/RootView.swift +25 -15
- package/templates/mac/consumer/apps/mac/Sources/__APP_NAME_PASCAL__/__APP_NAME_PASCAL__App.swift +34 -6
- package/templates/mac/consumer/apps/mac/project.yml +2 -0
- package/templates/web/chat/apps/web/src/app/components/ChatRoom.tsx +52 -71
- package/templates/web/chat/apps/web/src/app/layout.tsx +3 -2
- package/templates/web/chat/apps/web/src/app/page.tsx +5 -44
- package/templates/web/chat/apps/web/src/app/providers.tsx +25 -0
- package/templates/web/consumer/apps/web/src/app/components/Feed.tsx +139 -119
- package/templates/web/consumer/apps/web/src/app/layout.tsx +3 -2
- package/templates/web/consumer/apps/web/src/app/page.tsx +8 -42
- package/templates/web/consumer/apps/web/src/app/providers.tsx +25 -0
|
@@ -1,15 +1,14 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
|
-
import {
|
|
3
|
+
import { useMemo, useState } from "react";
|
|
4
|
+
import { db, callFn } from "@pylonsync/react";
|
|
4
5
|
import { Button, Input, Card, CardHeader, CardContent } from "@__APP_NAME_KEBAB__/ui";
|
|
5
6
|
|
|
6
|
-
type
|
|
7
|
+
type Post = {
|
|
7
8
|
id: string;
|
|
9
|
+
authorId: string;
|
|
8
10
|
body: string;
|
|
9
11
|
createdAt: string;
|
|
10
|
-
author: { id: string; handle: string; displayName: string } | null;
|
|
11
|
-
likeCount: number;
|
|
12
|
-
likedByMe: boolean;
|
|
13
12
|
};
|
|
14
13
|
|
|
15
14
|
type Profile = {
|
|
@@ -21,88 +20,109 @@ type Profile = {
|
|
|
21
20
|
createdAt: string;
|
|
22
21
|
};
|
|
23
22
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
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,
|
|
27
88
|
}: {
|
|
28
|
-
|
|
29
|
-
|
|
89
|
+
me: Profile;
|
|
90
|
+
items: Array<{
|
|
91
|
+
post: Post;
|
|
92
|
+
author: Profile | null;
|
|
93
|
+
likeCount: number;
|
|
94
|
+
likedByMe: boolean;
|
|
95
|
+
}>;
|
|
30
96
|
}) {
|
|
31
|
-
const [feed, setFeed] = useState(initialFeed);
|
|
32
|
-
const [me, setMe] = useState(initialMe);
|
|
33
97
|
const [body, setBody] = useState("");
|
|
34
|
-
const [
|
|
35
|
-
|
|
36
|
-
if (!me) {
|
|
37
|
-
return <ProfileSetup onSaved={(p) => setMe(p)} />;
|
|
38
|
-
}
|
|
98
|
+
const [posting, setPosting] = useState(false);
|
|
39
99
|
|
|
40
100
|
async function post() {
|
|
41
101
|
const trimmed = body.trim();
|
|
42
102
|
if (!trimmed) return;
|
|
43
103
|
setBody("");
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
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
|
-
});
|
|
104
|
+
setPosting(true);
|
|
105
|
+
try {
|
|
106
|
+
await callFn("createPost", { body: trimmed });
|
|
107
|
+
} finally {
|
|
108
|
+
setPosting(false);
|
|
109
|
+
}
|
|
57
110
|
}
|
|
58
111
|
|
|
59
|
-
async function toggleLike(
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
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
|
-
});
|
|
112
|
+
async function toggleLike(postId: string) {
|
|
113
|
+
try {
|
|
114
|
+
await callFn("toggleLike", { postId });
|
|
115
|
+
} catch (e) {
|
|
116
|
+
window.alert(`Like failed: ${String(e)}`);
|
|
117
|
+
}
|
|
93
118
|
}
|
|
94
119
|
|
|
95
|
-
async function remove(
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
headers: { "Content-Type": "application/json" },
|
|
102
|
-
body: JSON.stringify({ id: item.id }),
|
|
103
|
-
});
|
|
104
|
-
if (!res.ok) setFeed(snapshot);
|
|
105
|
-
});
|
|
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
|
+
}
|
|
106
126
|
}
|
|
107
127
|
|
|
108
128
|
return (
|
|
@@ -116,12 +136,6 @@ export function Feed({
|
|
|
116
136
|
@{me.handle}
|
|
117
137
|
</div>
|
|
118
138
|
</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
139
|
</div>
|
|
126
140
|
</CardHeader>
|
|
127
141
|
<CardContent>
|
|
@@ -138,7 +152,7 @@ export function Feed({
|
|
|
138
152
|
placeholder={`What's on your mind, ${me.displayName.split(" ")[0]}?`}
|
|
139
153
|
rows={3}
|
|
140
154
|
maxLength={1000}
|
|
141
|
-
disabled={
|
|
155
|
+
disabled={posting}
|
|
142
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"
|
|
143
157
|
/>
|
|
144
158
|
<div className="flex items-center justify-between">
|
|
@@ -148,7 +162,7 @@ export function Feed({
|
|
|
148
162
|
<Button
|
|
149
163
|
type="submit"
|
|
150
164
|
variant="primary"
|
|
151
|
-
disabled={
|
|
165
|
+
disabled={posting || !body.trim()}
|
|
152
166
|
size="sm"
|
|
153
167
|
>
|
|
154
168
|
Post
|
|
@@ -158,27 +172,27 @@ export function Feed({
|
|
|
158
172
|
</CardContent>
|
|
159
173
|
</Card>
|
|
160
174
|
|
|
161
|
-
{
|
|
175
|
+
{items.length === 0 ? (
|
|
162
176
|
<p className="text-sm text-neutral-500 text-center py-8">
|
|
163
177
|
No posts yet. Be the first.
|
|
164
178
|
</p>
|
|
165
179
|
) : (
|
|
166
180
|
<ul className="space-y-3">
|
|
167
|
-
{
|
|
168
|
-
<li key={
|
|
181
|
+
{items.map(({ post, author, likeCount, likedByMe }) => (
|
|
182
|
+
<li key={post.id}>
|
|
169
183
|
<Card>
|
|
170
184
|
<CardContent className="space-y-3">
|
|
171
185
|
<div className="flex items-baseline justify-between">
|
|
172
186
|
<div className="text-sm">
|
|
173
187
|
<span className="font-medium">
|
|
174
|
-
{
|
|
188
|
+
{author?.displayName ?? "Unknown"}
|
|
175
189
|
</span>{" "}
|
|
176
190
|
<span className="text-neutral-400 font-mono text-xs">
|
|
177
|
-
@{
|
|
191
|
+
@{author?.handle ?? "?"}
|
|
178
192
|
</span>
|
|
179
193
|
</div>
|
|
180
194
|
<span className="text-xs text-neutral-400">
|
|
181
|
-
{new Date(
|
|
195
|
+
{new Date(post.createdAt).toLocaleString(undefined, {
|
|
182
196
|
month: "short",
|
|
183
197
|
day: "numeric",
|
|
184
198
|
hour: "numeric",
|
|
@@ -187,22 +201,22 @@ export function Feed({
|
|
|
187
201
|
</span>
|
|
188
202
|
</div>
|
|
189
203
|
<p className="text-sm whitespace-pre-wrap break-words">
|
|
190
|
-
{
|
|
204
|
+
{post.body}
|
|
191
205
|
</p>
|
|
192
206
|
<div className="flex items-center gap-2">
|
|
193
207
|
<button
|
|
194
|
-
onClick={() => toggleLike(
|
|
208
|
+
onClick={() => toggleLike(post.id)}
|
|
195
209
|
className={`text-xs font-mono px-2 py-1 rounded border transition-colors ${
|
|
196
|
-
|
|
210
|
+
likedByMe
|
|
197
211
|
? "border-pink-300 dark:border-pink-700 text-pink-600 dark:text-pink-300 bg-pink-50 dark:bg-pink-950"
|
|
198
212
|
: "border-neutral-200 dark:border-neutral-800 text-neutral-500 hover:bg-neutral-50 dark:hover:bg-neutral-900"
|
|
199
213
|
}`}
|
|
200
214
|
>
|
|
201
|
-
{
|
|
215
|
+
{likedByMe ? "♥" : "♡"} {likeCount}
|
|
202
216
|
</button>
|
|
203
|
-
{
|
|
217
|
+
{author?.id === me.id && (
|
|
204
218
|
<button
|
|
205
|
-
onClick={() => remove(
|
|
219
|
+
onClick={() => remove(post.id)}
|
|
206
220
|
className="text-xs text-neutral-400 hover:text-red-500"
|
|
207
221
|
>
|
|
208
222
|
Delete
|
|
@@ -219,30 +233,40 @@ export function Feed({
|
|
|
219
233
|
);
|
|
220
234
|
}
|
|
221
235
|
|
|
222
|
-
function ProfileSetup({
|
|
236
|
+
function ProfileSetup({
|
|
237
|
+
existingHandles,
|
|
238
|
+
onSaved,
|
|
239
|
+
}: {
|
|
240
|
+
existingHandles: string[];
|
|
241
|
+
onSaved: (p: Profile) => void;
|
|
242
|
+
}) {
|
|
223
243
|
const [handle, setHandle] = useState("");
|
|
224
244
|
const [displayName, setDisplayName] = useState("");
|
|
225
245
|
const [bio, setBio] = useState("");
|
|
226
|
-
const [
|
|
246
|
+
const [saving, setSaving] = useState(false);
|
|
227
247
|
const [error, setError] = useState<string | null>(null);
|
|
228
248
|
|
|
229
249
|
async function save(e: React.FormEvent) {
|
|
230
250
|
e.preventDefault();
|
|
231
251
|
setError(null);
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
})
|
|
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
|
+
}
|
|
246
270
|
}
|
|
247
271
|
|
|
248
272
|
return (
|
|
@@ -253,9 +277,7 @@ function ProfileSetup({ onSaved }: { onSaved: (p: Profile) => void }) {
|
|
|
253
277
|
<CardContent>
|
|
254
278
|
<form onSubmit={save} className="space-y-3">
|
|
255
279
|
<div>
|
|
256
|
-
<label className="text-xs text-neutral-500 block mb-1">
|
|
257
|
-
Handle
|
|
258
|
-
</label>
|
|
280
|
+
<label className="text-xs text-neutral-500 block mb-1">Handle</label>
|
|
259
281
|
<Input
|
|
260
282
|
value={handle}
|
|
261
283
|
onChange={(e) => setHandle(e.target.value.toLowerCase())}
|
|
@@ -275,9 +297,7 @@ function ProfileSetup({ onSaved }: { onSaved: (p: Profile) => void }) {
|
|
|
275
297
|
/>
|
|
276
298
|
</div>
|
|
277
299
|
<div>
|
|
278
|
-
<label className="text-xs text-neutral-500 block mb-1">
|
|
279
|
-
Bio
|
|
280
|
-
</label>
|
|
300
|
+
<label className="text-xs text-neutral-500 block mb-1">Bio</label>
|
|
281
301
|
<Input
|
|
282
302
|
value={bio}
|
|
283
303
|
onChange={(e) => setBio(e.target.value)}
|
|
@@ -285,8 +305,8 @@ function ProfileSetup({ onSaved }: { onSaved: (p: Profile) => void }) {
|
|
|
285
305
|
/>
|
|
286
306
|
</div>
|
|
287
307
|
{error && <p className="text-xs text-red-500">{error}</p>}
|
|
288
|
-
<Button type="submit" variant="primary" disabled={
|
|
289
|
-
{
|
|
308
|
+
<Button type="submit" variant="primary" disabled={saving}>
|
|
309
|
+
{saving ? "Saving…" : "Save"}
|
|
290
310
|
</Button>
|
|
291
311
|
</form>
|
|
292
312
|
</CardContent>
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import type { Metadata } from "next";
|
|
2
2
|
import "./globals.css";
|
|
3
|
+
import { Providers } from "./providers";
|
|
3
4
|
|
|
4
5
|
export const metadata: Metadata = {
|
|
5
6
|
title: "__APP_NAME__",
|
|
6
|
-
description: "
|
|
7
|
+
description: "Realtime social feed powered by Pylon",
|
|
7
8
|
};
|
|
8
9
|
|
|
9
10
|
export default function RootLayout({
|
|
@@ -14,7 +15,7 @@ export default function RootLayout({
|
|
|
14
15
|
return (
|
|
15
16
|
<html lang="en">
|
|
16
17
|
<body className="antialiased min-h-screen bg-white dark:bg-neutral-950 text-neutral-900 dark:text-neutral-100">
|
|
17
|
-
{children}
|
|
18
|
+
<Providers>{children}</Providers>
|
|
18
19
|
</body>
|
|
19
20
|
</html>
|
|
20
21
|
);
|
|
@@ -1,55 +1,21 @@
|
|
|
1
|
-
import { pylon } from "@/lib/pylon";
|
|
2
1
|
import { Feed } from "./components/Feed";
|
|
3
2
|
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
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
|
-
|
|
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() {
|
|
42
8
|
return (
|
|
43
9
|
<main className="mx-auto max-w-2xl px-6 py-12 space-y-8">
|
|
44
10
|
<header className="space-y-2">
|
|
45
11
|
<h1 className="text-3xl font-semibold tracking-tight">__APP_NAME__</h1>
|
|
46
12
|
<p className="text-sm text-neutral-500 dark:text-neutral-400">
|
|
47
|
-
Public feed
|
|
48
|
-
|
|
13
|
+
Public feed. Profile / Post / Like — wide-open reads, owner-only
|
|
14
|
+
writes. Live updates via Pylon's sync engine.
|
|
49
15
|
</p>
|
|
50
16
|
</header>
|
|
51
17
|
|
|
52
|
-
<Feed
|
|
18
|
+
<Feed />
|
|
53
19
|
</main>
|
|
54
20
|
);
|
|
55
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
|
+
}
|