@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.
- package/bin/create-pylon.js +98 -42
- package/package.json +1 -1
- package/templates/backend/b2b/apps/api/functions/archiveProject.ts +15 -0
- package/templates/backend/b2b/apps/api/functions/createOrg.ts +43 -0
- package/templates/backend/b2b/apps/api/functions/createProject.ts +25 -0
- package/templates/backend/b2b/apps/api/functions/inviteMember.ts +49 -0
- package/templates/backend/b2b/apps/api/functions/myOrgs.ts +37 -0
- package/templates/backend/b2b/apps/api/functions/orgMembers.ts +13 -0
- package/templates/backend/b2b/apps/api/functions/orgProjects.ts +18 -0
- package/templates/backend/b2b/apps/api/functions/removeMember.ts +29 -0
- package/templates/backend/b2b/apps/api/functions/setMemberRole.ts +38 -0
- package/templates/backend/b2b/apps/api/package.json +20 -0
- package/templates/backend/b2b/apps/api/schema.ts +171 -0
- package/templates/backend/b2b/apps/api/tsconfig.json +13 -0
- package/templates/backend/chat/apps/api/functions/createRoom.ts +32 -0
- package/templates/backend/chat/apps/api/functions/listRooms.ts +15 -0
- package/templates/backend/chat/apps/api/functions/roomMessages.ts +20 -0
- package/templates/backend/chat/apps/api/functions/sendMessage.ts +37 -0
- package/templates/backend/chat/apps/api/package.json +20 -0
- package/templates/backend/chat/apps/api/schema.ts +93 -0
- package/templates/backend/chat/apps/api/tsconfig.json +13 -0
- package/templates/backend/consumer/apps/api/functions/createPost.ts +48 -0
- package/templates/backend/consumer/apps/api/functions/deletePost.ts +21 -0
- package/templates/backend/consumer/apps/api/functions/feed.ts +57 -0
- package/templates/backend/consumer/apps/api/functions/myProfile.ts +17 -0
- package/templates/backend/consumer/apps/api/functions/profilePosts.ts +17 -0
- package/templates/backend/consumer/apps/api/functions/toggleLike.ts +48 -0
- package/templates/backend/consumer/apps/api/functions/upsertProfile.ts +70 -0
- package/templates/backend/consumer/apps/api/package.json +20 -0
- package/templates/backend/consumer/apps/api/schema.ts +130 -0
- package/templates/backend/consumer/apps/api/tsconfig.json +13 -0
- package/templates/expo/chat/apps/expo/App.tsx +384 -0
- package/templates/expo/chat/apps/expo/app.json +25 -0
- package/templates/expo/chat/apps/expo/babel.config.js +6 -0
- package/templates/expo/chat/apps/expo/package.json +30 -0
- package/templates/expo/chat/apps/expo/tsconfig.json +16 -0
- package/templates/expo/consumer/apps/expo/App.tsx +392 -0
- package/templates/expo/consumer/apps/expo/app.json +25 -0
- package/templates/expo/consumer/apps/expo/babel.config.js +6 -0
- package/templates/expo/consumer/apps/expo/package.json +30 -0
- package/templates/expo/consumer/apps/expo/tsconfig.json +16 -0
- package/templates/ios/chat/apps/ios/Package.swift +24 -0
- package/templates/ios/chat/apps/ios/Sources/__APP_NAME_PASCAL__/ChatRootView.swift +116 -0
- package/templates/ios/chat/apps/ios/Sources/__APP_NAME_PASCAL__/Models.swift +26 -0
- package/templates/ios/chat/apps/ios/Sources/__APP_NAME_PASCAL__/RoomView.swift +136 -0
- package/templates/ios/chat/apps/ios/Sources/__APP_NAME_PASCAL__/__APP_NAME_PASCAL__App.swift +58 -0
- package/templates/ios/chat/apps/ios/project.yml +44 -0
- package/templates/ios/consumer/apps/ios/Package.swift +24 -0
- package/templates/ios/consumer/apps/ios/Sources/__APP_NAME_PASCAL__/FeedView.swift +199 -0
- package/templates/ios/consumer/apps/ios/Sources/__APP_NAME_PASCAL__/Models.swift +34 -0
- package/templates/ios/consumer/apps/ios/Sources/__APP_NAME_PASCAL__/ProfileSetupView.swift +68 -0
- package/templates/ios/consumer/apps/ios/Sources/__APP_NAME_PASCAL__/RootView.swift +40 -0
- package/templates/ios/consumer/apps/ios/Sources/__APP_NAME_PASCAL__/__APP_NAME_PASCAL__App.swift +57 -0
- package/templates/ios/consumer/apps/ios/project.yml +44 -0
- package/templates/mac/b2b/apps/mac/Package.swift +22 -0
- package/templates/mac/b2b/apps/mac/Sources/__APP_NAME_PASCAL__/Models.swift +15 -0
- package/templates/mac/b2b/apps/mac/Sources/__APP_NAME_PASCAL__/OrgPickerView.swift +178 -0
- package/templates/mac/b2b/apps/mac/Sources/__APP_NAME_PASCAL__/__APP_NAME_PASCAL__App.swift +30 -0
- package/templates/mac/b2b/apps/mac/__APP_NAME_PASCAL__.entitlements +13 -0
- package/templates/mac/b2b/apps/mac/project.yml +34 -0
- package/templates/mac/barebones/apps/mac/Package.swift +33 -0
- package/templates/mac/barebones/apps/mac/Sources/__APP_NAME_PASCAL__/ContentView.swift +104 -0
- package/templates/mac/barebones/apps/mac/Sources/__APP_NAME_PASCAL__/Models.swift +17 -0
- package/templates/mac/barebones/apps/mac/Sources/__APP_NAME_PASCAL__/__APP_NAME_PASCAL__App.swift +30 -0
- package/templates/mac/barebones/apps/mac/__APP_NAME_PASCAL__.entitlements +13 -0
- package/templates/mac/barebones/apps/mac/project.yml +34 -0
- package/templates/mac/chat/apps/mac/Package.swift +34 -0
- package/templates/mac/chat/apps/mac/Sources/__APP_NAME_PASCAL__/ChatRootView.swift +143 -0
- package/templates/mac/chat/apps/mac/Sources/__APP_NAME_PASCAL__/Models.swift +26 -0
- package/templates/mac/chat/apps/mac/Sources/__APP_NAME_PASCAL__/RoomView.swift +136 -0
- package/templates/mac/chat/apps/mac/Sources/__APP_NAME_PASCAL__/__APP_NAME_PASCAL__App.swift +58 -0
- package/templates/mac/chat/apps/mac/__APP_NAME_PASCAL__.entitlements +13 -0
- package/templates/mac/chat/apps/mac/project.yml +36 -0
- package/templates/mac/consumer/apps/mac/Package.swift +34 -0
- package/templates/mac/consumer/apps/mac/Sources/__APP_NAME_PASCAL__/FeedView.swift +199 -0
- package/templates/mac/consumer/apps/mac/Sources/__APP_NAME_PASCAL__/Models.swift +34 -0
- package/templates/mac/consumer/apps/mac/Sources/__APP_NAME_PASCAL__/ProfileSetupView.swift +68 -0
- package/templates/mac/consumer/apps/mac/Sources/__APP_NAME_PASCAL__/RootView.swift +40 -0
- package/templates/mac/consumer/apps/mac/Sources/__APP_NAME_PASCAL__/__APP_NAME_PASCAL__App.swift +59 -0
- package/templates/mac/consumer/apps/mac/__APP_NAME_PASCAL__.entitlements +13 -0
- package/templates/mac/consumer/apps/mac/project.yml +36 -0
- package/templates/mac/todo/apps/mac/Package.swift +33 -0
- package/templates/mac/todo/apps/mac/Sources/__APP_NAME_PASCAL__/Models.swift +19 -0
- package/templates/mac/todo/apps/mac/Sources/__APP_NAME_PASCAL__/TodoListView.swift +244 -0
- package/templates/mac/todo/apps/mac/Sources/__APP_NAME_PASCAL__/__APP_NAME_PASCAL__App.swift +30 -0
- package/templates/mac/todo/apps/mac/__APP_NAME_PASCAL__.entitlements +13 -0
- package/templates/mac/todo/apps/mac/project.yml +34 -0
- package/templates/web/b2b/apps/web/next-env.d.ts +2 -0
- package/templates/web/b2b/apps/web/next.config.ts +24 -0
- package/templates/web/b2b/apps/web/package.json +29 -0
- package/templates/web/b2b/apps/web/postcss.config.mjs +3 -0
- package/templates/web/b2b/apps/web/src/app/components/OrgPicker.tsx +171 -0
- package/templates/web/b2b/apps/web/src/app/globals.css +6 -0
- package/templates/web/b2b/apps/web/src/app/layout.tsx +21 -0
- package/templates/web/b2b/apps/web/src/app/page.tsx +39 -0
- package/templates/web/b2b/apps/web/src/lib/pylon.ts +5 -0
- package/templates/web/b2b/apps/web/tsconfig.json +26 -0
- package/templates/web/chat/apps/web/next-env.d.ts +2 -0
- package/templates/web/chat/apps/web/next.config.ts +24 -0
- package/templates/web/chat/apps/web/package.json +29 -0
- package/templates/web/chat/apps/web/postcss.config.mjs +3 -0
- package/templates/web/chat/apps/web/src/app/components/ChatRoom.tsx +231 -0
- package/templates/web/chat/apps/web/src/app/globals.css +6 -0
- package/templates/web/chat/apps/web/src/app/layout.tsx +22 -0
- package/templates/web/chat/apps/web/src/app/page.tsx +12 -0
- package/templates/web/chat/apps/web/src/app/providers.tsx +25 -0
- package/templates/web/chat/apps/web/src/lib/pylon.ts +5 -0
- package/templates/web/chat/apps/web/tsconfig.json +26 -0
- package/templates/web/consumer/apps/web/next-env.d.ts +2 -0
- package/templates/web/consumer/apps/web/next.config.ts +24 -0
- package/templates/web/consumer/apps/web/package.json +29 -0
- package/templates/web/consumer/apps/web/postcss.config.mjs +3 -0
- package/templates/web/consumer/apps/web/src/app/components/Feed.tsx +315 -0
- package/templates/web/consumer/apps/web/src/app/globals.css +6 -0
- package/templates/web/consumer/apps/web/src/app/layout.tsx +22 -0
- package/templates/web/consumer/apps/web/src/app/page.tsx +21 -0
- package/templates/web/consumer/apps/web/src/app/providers.tsx +25 -0
- package/templates/web/consumer/apps/web/src/lib/pylon.ts +5 -0
- package/templates/web/consumer/apps/web/tsconfig.json +26 -0
- /package/templates/{mobile/barebones/apps/mobile → ios/barebones/apps/ios}/Package.swift +0 -0
- /package/templates/{mobile/barebones/apps/mobile → ios/barebones/apps/ios}/Sources/__APP_NAME_PASCAL__/ContentView.swift +0 -0
- /package/templates/{mobile/barebones/apps/mobile → ios/barebones/apps/ios}/Sources/__APP_NAME_PASCAL__/Models.swift +0 -0
- /package/templates/{mobile/barebones/apps/mobile → ios/barebones/apps/ios}/Sources/__APP_NAME_PASCAL__/__APP_NAME_PASCAL__App.swift +0 -0
- /package/templates/{mobile/barebones/apps/mobile → ios/barebones/apps/ios}/project.yml +0 -0
- /package/templates/{mobile/todo/apps/mobile → ios/todo/apps/ios}/Package.swift +0 -0
- /package/templates/{mobile/todo/apps/mobile → ios/todo/apps/ios}/Sources/__APP_NAME_PASCAL__/Models.swift +0 -0
- /package/templates/{mobile/todo/apps/mobile → ios/todo/apps/ios}/Sources/__APP_NAME_PASCAL__/TodoListView.swift +0 -0
- /package/templates/{mobile/todo/apps/mobile → ios/todo/apps/ios}/Sources/__APP_NAME_PASCAL__/__APP_NAME_PASCAL__App.swift +0 -0
- /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,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,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'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,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
|
+
}
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|