@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.
- package/bin/create-pylon.js +347 -1156
- package/package.json +4 -3
- package/templates/_root/.env.example +9 -0
- package/templates/_root/README.md +43 -0
- 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/barebones/apps/api/functions/createWidget.ts +22 -0
- package/templates/backend/barebones/apps/api/functions/listWidgets.ts +18 -0
- package/templates/backend/barebones/apps/api/package.json +20 -0
- package/templates/backend/barebones/apps/api/schema.ts +61 -0
- package/templates/backend/barebones/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/backend/todo/apps/api/functions/addTodo.ts +27 -0
- package/templates/backend/todo/apps/api/functions/deleteTodo.ts +14 -0
- package/templates/backend/todo/apps/api/functions/editTodo.ts +16 -0
- package/templates/backend/todo/apps/api/functions/listTodos.ts +24 -0
- package/templates/backend/todo/apps/api/functions/reorderTodo.ts +14 -0
- package/templates/backend/todo/apps/api/functions/toggleTodo.ts +13 -0
- package/templates/backend/todo/apps/api/package.json +20 -0
- package/templates/backend/todo/apps/api/schema.ts +85 -0
- package/templates/backend/todo/apps/api/tsconfig.json +13 -0
- package/templates/expo/barebones/apps/expo/App.tsx +166 -0
- package/templates/expo/barebones/apps/expo/app.json +31 -0
- package/templates/expo/barebones/apps/expo/babel.config.js +6 -0
- package/templates/expo/barebones/apps/expo/package.json +30 -0
- package/templates/expo/barebones/apps/expo/tsconfig.json +16 -0
- package/templates/expo/chat/apps/expo/App.tsx +414 -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 +360 -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/expo/todo/apps/expo/App.tsx +287 -0
- package/templates/expo/todo/apps/expo/app.json +25 -0
- package/templates/expo/todo/apps/expo/babel.config.js +6 -0
- package/templates/expo/todo/apps/expo/package.json +30 -0
- package/templates/expo/todo/apps/expo/tsconfig.json +16 -0
- package/templates/ios/barebones/apps/ios/Package.swift +34 -0
- package/templates/ios/barebones/apps/ios/Sources/__APP_NAME_PASCAL__/ContentView.swift +98 -0
- package/templates/ios/barebones/apps/ios/Sources/__APP_NAME_PASCAL__/Models.swift +17 -0
- package/templates/ios/barebones/apps/ios/Sources/__APP_NAME_PASCAL__/__APP_NAME_PASCAL__App.swift +34 -0
- package/templates/ios/barebones/apps/ios/project.yml +42 -0
- package/templates/ios/chat/apps/ios/Package.swift +34 -0
- package/templates/ios/chat/apps/ios/Sources/__APP_NAME_PASCAL__/ChatRootView.swift +120 -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 +137 -0
- package/templates/ios/chat/apps/ios/Sources/__APP_NAME_PASCAL__/__APP_NAME_PASCAL__App.swift +35 -0
- package/templates/ios/chat/apps/ios/project.yml +42 -0
- package/templates/ios/consumer/apps/ios/Package.swift +23 -0
- package/templates/ios/consumer/apps/ios/Sources/__APP_NAME_PASCAL__/FeedView.swift +170 -0
- package/templates/ios/consumer/apps/ios/Sources/__APP_NAME_PASCAL__/Models.swift +42 -0
- package/templates/ios/consumer/apps/ios/Sources/__APP_NAME_PASCAL__/ProfileSetupView.swift +60 -0
- package/templates/ios/consumer/apps/ios/Sources/__APP_NAME_PASCAL__/RootView.swift +30 -0
- package/templates/ios/consumer/apps/ios/Sources/__APP_NAME_PASCAL__/__APP_NAME_PASCAL__App.swift +29 -0
- package/templates/ios/consumer/apps/ios/project.yml +42 -0
- package/templates/ios/todo/apps/ios/Package.swift +23 -0
- package/templates/ios/todo/apps/ios/Sources/__APP_NAME_PASCAL__/Models.swift +18 -0
- package/templates/ios/todo/apps/ios/Sources/__APP_NAME_PASCAL__/TodoListView.swift +230 -0
- package/templates/ios/todo/apps/ios/Sources/__APP_NAME_PASCAL__/__APP_NAME_PASCAL__App.swift +28 -0
- package/templates/ios/todo/apps/ios/project.yml +32 -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 +33 -0
- package/templates/mac/chat/apps/mac/Sources/__APP_NAME_PASCAL__/ChatRootView.swift +140 -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 +137 -0
- package/templates/mac/chat/apps/mac/Sources/__APP_NAME_PASCAL__/__APP_NAME_PASCAL__App.swift +37 -0
- package/templates/mac/chat/apps/mac/__APP_NAME_PASCAL__.entitlements +13 -0
- package/templates/mac/chat/apps/mac/project.yml +34 -0
- package/templates/mac/consumer/apps/mac/Package.swift +33 -0
- package/templates/mac/consumer/apps/mac/Sources/__APP_NAME_PASCAL__/FeedView.swift +170 -0
- package/templates/mac/consumer/apps/mac/Sources/__APP_NAME_PASCAL__/Models.swift +42 -0
- package/templates/mac/consumer/apps/mac/Sources/__APP_NAME_PASCAL__/ProfileSetupView.swift +60 -0
- package/templates/mac/consumer/apps/mac/Sources/__APP_NAME_PASCAL__/RootView.swift +30 -0
- package/templates/mac/consumer/apps/mac/Sources/__APP_NAME_PASCAL__/__APP_NAME_PASCAL__App.swift +31 -0
- package/templates/mac/consumer/apps/mac/__APP_NAME_PASCAL__.entitlements +13 -0
- package/templates/mac/consumer/apps/mac/project.yml +34 -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/ui/packages/ui/package.json +26 -0
- package/templates/ui/packages/ui/src/button.tsx +44 -0
- package/templates/ui/packages/ui/src/card.tsx +39 -0
- package/templates/ui/packages/ui/src/cn.ts +12 -0
- package/templates/ui/packages/ui/src/index.ts +4 -0
- package/templates/ui/packages/ui/src/input.tsx +19 -0
- package/templates/ui/packages/ui/tsconfig.json +15 -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/barebones/apps/web/next-env.d.ts +2 -0
- package/templates/web/barebones/apps/web/next.config.ts +40 -0
- package/templates/web/barebones/apps/web/package.json +29 -0
- package/templates/web/barebones/apps/web/postcss.config.mjs +4 -0
- package/templates/web/barebones/apps/web/src/app/components/WidgetList.tsx +81 -0
- package/templates/web/barebones/apps/web/src/app/globals.css +9 -0
- package/templates/web/barebones/apps/web/src/app/layout.tsx +21 -0
- package/templates/web/barebones/apps/web/src/app/page.tsx +43 -0
- package/templates/web/barebones/apps/web/src/lib/pylon.ts +14 -0
- package/templates/web/barebones/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 +250 -0
- package/templates/web/chat/apps/web/src/app/globals.css +6 -0
- package/templates/web/chat/apps/web/src/app/layout.tsx +21 -0
- package/templates/web/chat/apps/web/src/app/page.tsx +51 -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 +295 -0
- package/templates/web/consumer/apps/web/src/app/globals.css +6 -0
- package/templates/web/consumer/apps/web/src/app/layout.tsx +21 -0
- package/templates/web/consumer/apps/web/src/app/page.tsx +55 -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/web/todo/apps/web/next-env.d.ts +2 -0
- package/templates/web/todo/apps/web/next.config.ts +24 -0
- package/templates/web/todo/apps/web/package.json +32 -0
- package/templates/web/todo/apps/web/postcss.config.mjs +3 -0
- package/templates/web/todo/apps/web/src/app/components/TodoList.tsx +310 -0
- package/templates/web/todo/apps/web/src/app/globals.css +6 -0
- package/templates/web/todo/apps/web/src/app/layout.tsx +21 -0
- package/templates/web/todo/apps/web/src/app/page.tsx +36 -0
- package/templates/web/todo/apps/web/src/lib/pylon.ts +5 -0
- package/templates/web/todo/apps/web/tsconfig.json +26 -0
|
@@ -0,0 +1,171 @@
|
|
|
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 Org = {
|
|
7
|
+
id: string;
|
|
8
|
+
slug: string;
|
|
9
|
+
name: string;
|
|
10
|
+
role: string;
|
|
11
|
+
createdAt: string;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export function OrgPicker({ initialOrgs }: { initialOrgs: Org[] }) {
|
|
15
|
+
const [orgs, setOrgs] = useState(initialOrgs);
|
|
16
|
+
const [activeId, setActiveId] = useState<string | null>(
|
|
17
|
+
initialOrgs[0]?.id ?? null,
|
|
18
|
+
);
|
|
19
|
+
const [creating, setCreating] = useState(false);
|
|
20
|
+
const [name, setName] = useState("");
|
|
21
|
+
const [slug, setSlug] = useState("");
|
|
22
|
+
const [pending, startTransition] = useTransition();
|
|
23
|
+
const [error, setError] = useState<string | null>(null);
|
|
24
|
+
|
|
25
|
+
async function create(e: React.FormEvent) {
|
|
26
|
+
e.preventDefault();
|
|
27
|
+
setError(null);
|
|
28
|
+
startTransition(async () => {
|
|
29
|
+
const res = await fetch("/api/fn/createOrg", {
|
|
30
|
+
method: "POST",
|
|
31
|
+
headers: { "Content-Type": "application/json" },
|
|
32
|
+
body: JSON.stringify({ slug, name }),
|
|
33
|
+
});
|
|
34
|
+
if (res.ok) {
|
|
35
|
+
const org = (await res.json()) as Org;
|
|
36
|
+
setOrgs((prev) => [
|
|
37
|
+
{ ...org, role: "owner" },
|
|
38
|
+
...prev,
|
|
39
|
+
]);
|
|
40
|
+
setActiveId(org.id);
|
|
41
|
+
setName("");
|
|
42
|
+
setSlug("");
|
|
43
|
+
setCreating(false);
|
|
44
|
+
} else {
|
|
45
|
+
const body = await res.json().catch(() => ({}));
|
|
46
|
+
setError(body?.message ?? "create failed");
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const active = orgs.find((o) => o.id === activeId) ?? null;
|
|
52
|
+
|
|
53
|
+
return (
|
|
54
|
+
<div className="space-y-6">
|
|
55
|
+
<Card>
|
|
56
|
+
<CardHeader>
|
|
57
|
+
<div className="flex items-center justify-between">
|
|
58
|
+
<h2 className="text-sm font-medium">Your organizations</h2>
|
|
59
|
+
<Button
|
|
60
|
+
size="sm"
|
|
61
|
+
variant={creating ? "ghost" : "default"}
|
|
62
|
+
onClick={() => setCreating((v) => !v)}
|
|
63
|
+
>
|
|
64
|
+
{creating ? "Cancel" : "New org"}
|
|
65
|
+
</Button>
|
|
66
|
+
</div>
|
|
67
|
+
</CardHeader>
|
|
68
|
+
<CardContent>
|
|
69
|
+
{creating && (
|
|
70
|
+
<form onSubmit={create} className="grid grid-cols-2 gap-2 mb-4">
|
|
71
|
+
<Input
|
|
72
|
+
placeholder="Org name"
|
|
73
|
+
value={name}
|
|
74
|
+
onChange={(e) => setName(e.target.value)}
|
|
75
|
+
disabled={pending}
|
|
76
|
+
required
|
|
77
|
+
/>
|
|
78
|
+
<Input
|
|
79
|
+
placeholder="acme-corp"
|
|
80
|
+
value={slug}
|
|
81
|
+
onChange={(e) => setSlug(e.target.value.toLowerCase())}
|
|
82
|
+
disabled={pending}
|
|
83
|
+
pattern="[a-z0-9][a-z0-9-]{1,30}[a-z0-9]"
|
|
84
|
+
required
|
|
85
|
+
/>
|
|
86
|
+
<Button
|
|
87
|
+
type="submit"
|
|
88
|
+
variant="primary"
|
|
89
|
+
disabled={pending || !name.trim() || !slug.trim()}
|
|
90
|
+
className="col-span-2"
|
|
91
|
+
>
|
|
92
|
+
{pending ? "Creating…" : "Create org"}
|
|
93
|
+
</Button>
|
|
94
|
+
{error && (
|
|
95
|
+
<p className="text-xs text-red-500 col-span-2">{error}</p>
|
|
96
|
+
)}
|
|
97
|
+
</form>
|
|
98
|
+
)}
|
|
99
|
+
|
|
100
|
+
{orgs.length === 0 ? (
|
|
101
|
+
<p className="text-sm text-neutral-500 py-2">
|
|
102
|
+
You're not in any orgs yet. Create one above to get started.
|
|
103
|
+
</p>
|
|
104
|
+
) : (
|
|
105
|
+
<ul className="divide-y divide-neutral-200 dark:divide-neutral-800 -mx-5">
|
|
106
|
+
{orgs.map((o) => (
|
|
107
|
+
<li
|
|
108
|
+
key={o.id}
|
|
109
|
+
className="flex items-center justify-between px-5 py-2.5 cursor-pointer hover:bg-neutral-50 dark:hover:bg-neutral-900"
|
|
110
|
+
data-active={o.id === activeId || undefined}
|
|
111
|
+
onClick={() => setActiveId(o.id)}
|
|
112
|
+
>
|
|
113
|
+
<div>
|
|
114
|
+
<div className="text-sm font-medium">{o.name}</div>
|
|
115
|
+
<div className="text-xs text-neutral-400 font-mono">
|
|
116
|
+
{o.slug}
|
|
117
|
+
</div>
|
|
118
|
+
</div>
|
|
119
|
+
<span
|
|
120
|
+
className={`text-xs font-mono uppercase tracking-wide px-2 py-0.5 rounded ${
|
|
121
|
+
o.role === "owner"
|
|
122
|
+
? "bg-blue-100 text-blue-700 dark:bg-blue-950 dark:text-blue-300"
|
|
123
|
+
: o.role === "admin"
|
|
124
|
+
? "bg-amber-100 text-amber-700 dark:bg-amber-950 dark:text-amber-300"
|
|
125
|
+
: "bg-neutral-100 text-neutral-600 dark:bg-neutral-800 dark:text-neutral-400"
|
|
126
|
+
}`}
|
|
127
|
+
>
|
|
128
|
+
{o.role}
|
|
129
|
+
</span>
|
|
130
|
+
</li>
|
|
131
|
+
))}
|
|
132
|
+
</ul>
|
|
133
|
+
)}
|
|
134
|
+
</CardContent>
|
|
135
|
+
</Card>
|
|
136
|
+
|
|
137
|
+
{active && <OrgDetail org={active} />}
|
|
138
|
+
</div>
|
|
139
|
+
);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function OrgDetail({ org }: { org: Org }) {
|
|
143
|
+
return (
|
|
144
|
+
<Card>
|
|
145
|
+
<CardHeader>
|
|
146
|
+
<div className="flex items-baseline justify-between">
|
|
147
|
+
<h2 className="text-sm font-medium">
|
|
148
|
+
{org.name}{" "}
|
|
149
|
+
<span className="text-xs text-neutral-400 font-mono ml-1">
|
|
150
|
+
/{org.slug}
|
|
151
|
+
</span>
|
|
152
|
+
</h2>
|
|
153
|
+
<span className="text-xs text-neutral-500">your role: {org.role}</span>
|
|
154
|
+
</div>
|
|
155
|
+
</CardHeader>
|
|
156
|
+
<CardContent className="space-y-4">
|
|
157
|
+
<p className="text-sm text-neutral-500">
|
|
158
|
+
Wire member management and project lists into this panel by calling
|
|
159
|
+
<code className="font-mono text-xs mx-1">/api/fn/orgMembers</code>
|
|
160
|
+
and
|
|
161
|
+
<code className="font-mono text-xs mx-1">/api/fn/orgProjects</code>
|
|
162
|
+
with this org's id. Both queries enforce membership; you only
|
|
163
|
+
see what your role allows.
|
|
164
|
+
</p>
|
|
165
|
+
<div className="text-xs font-mono text-neutral-400">
|
|
166
|
+
orgId: {org.id}
|
|
167
|
+
</div>
|
|
168
|
+
</CardContent>
|
|
169
|
+
</Card>
|
|
170
|
+
);
|
|
171
|
+
}
|
|
@@ -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,39 @@
|
|
|
1
|
+
import { pylon } from "@/lib/pylon";
|
|
2
|
+
import { OrgPicker } from "./components/OrgPicker";
|
|
3
|
+
|
|
4
|
+
export const dynamic = "force-dynamic";
|
|
5
|
+
|
|
6
|
+
type Org = {
|
|
7
|
+
id: string;
|
|
8
|
+
slug: string;
|
|
9
|
+
name: string;
|
|
10
|
+
role: string;
|
|
11
|
+
createdAt: string;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export default async function HomePage() {
|
|
15
|
+
const orgs = await pylon
|
|
16
|
+
.json<Org[]>("/api/fn/myOrgs", {
|
|
17
|
+
method: "POST",
|
|
18
|
+
body: "{}",
|
|
19
|
+
headers: { "Content-Type": "application/json" },
|
|
20
|
+
})
|
|
21
|
+
.catch(() => [] as Org[]);
|
|
22
|
+
|
|
23
|
+
return (
|
|
24
|
+
<main className="mx-auto max-w-3xl px-6 py-12 space-y-8">
|
|
25
|
+
<header className="space-y-2">
|
|
26
|
+
<h1 className="text-3xl font-semibold tracking-tight">__APP_NAME__</h1>
|
|
27
|
+
<p className="text-sm text-neutral-500 dark:text-neutral-400">
|
|
28
|
+
Multi-tenant SaaS scaffold. Pick an org to manage its members and
|
|
29
|
+
projects, or create a new one. Tenant isolation is enforced
|
|
30
|
+
server-side by Pylon row-level policies — clients can't leak
|
|
31
|
+
data across orgs even if they fabricate <code className="font-mono text-xs">orgId</code>{" "}
|
|
32
|
+
in a request.
|
|
33
|
+
</p>
|
|
34
|
+
</header>
|
|
35
|
+
|
|
36
|
+
<OrgPicker initialOrgs={orgs} />
|
|
37
|
+
</main>
|
|
38
|
+
);
|
|
39
|
+
}
|
|
@@ -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,40 @@
|
|
|
1
|
+
import type { NextConfig } from "next";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Pylon's typed client + functions packages re-export across the
|
|
5
|
+
* server/client boundary AND the workspace UI package ships TSX.
|
|
6
|
+
* `transpilePackages` makes Next bundle them cleanly.
|
|
7
|
+
*
|
|
8
|
+
* `rewrites` proxies every Pylon-owned path (`/api/fn/*`,
|
|
9
|
+
* `/api/auth/*`, `/api/sync/*`, …) to the Pylon binary running on
|
|
10
|
+
* `PYLON_API_URL` (default http://localhost:4321). Without this,
|
|
11
|
+
* Next.js sees `/api/fn/createWidget` as a missing route and 404s
|
|
12
|
+
* before the request reaches Pylon.
|
|
13
|
+
*
|
|
14
|
+
* In production set `PYLON_API_URL` to wherever you've deployed the
|
|
15
|
+
* Pylon binary (Fly, Render, Railway, your own box). The browser
|
|
16
|
+
* still hits same-origin paths under your Next deployment, and Next
|
|
17
|
+
* forwards them server-side — no CORS, no extra DNS.
|
|
18
|
+
*/
|
|
19
|
+
const PYLON_API_URL = process.env.PYLON_API_URL ?? "http://localhost:4321";
|
|
20
|
+
|
|
21
|
+
const config: NextConfig = {
|
|
22
|
+
transpilePackages: [
|
|
23
|
+
"@__APP_NAME_KEBAB__/ui",
|
|
24
|
+
"@pylonsync/sdk",
|
|
25
|
+
"@pylonsync/react",
|
|
26
|
+
"@pylonsync/next",
|
|
27
|
+
"@pylonsync/functions",
|
|
28
|
+
"@pylonsync/sync",
|
|
29
|
+
],
|
|
30
|
+
async rewrites() {
|
|
31
|
+
return [
|
|
32
|
+
{ source: "/api/fn/:path*", destination: `${PYLON_API_URL}/api/fn/:path*` },
|
|
33
|
+
{ source: "/api/auth/:path*", destination: `${PYLON_API_URL}/api/auth/:path*` },
|
|
34
|
+
{ source: "/api/sync/:path*", destination: `${PYLON_API_URL}/api/sync/:path*` },
|
|
35
|
+
{ source: "/api/:path*", destination: `${PYLON_API_URL}/api/:path*` },
|
|
36
|
+
];
|
|
37
|
+
},
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
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,81 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useState, useTransition } from "react";
|
|
4
|
+
import { Button, Input } from "@__APP_NAME_KEBAB__/ui";
|
|
5
|
+
|
|
6
|
+
type Widget = {
|
|
7
|
+
id: string;
|
|
8
|
+
name: string;
|
|
9
|
+
count: number;
|
|
10
|
+
createdAt: string;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export function WidgetList({ initialWidgets }: { initialWidgets: Widget[] }) {
|
|
14
|
+
const [widgets, setWidgets] = useState(initialWidgets);
|
|
15
|
+
const [name, setName] = useState("");
|
|
16
|
+
const [pending, startTransition] = useTransition();
|
|
17
|
+
|
|
18
|
+
async function add() {
|
|
19
|
+
const trimmed = name.trim();
|
|
20
|
+
if (!trimmed) return;
|
|
21
|
+
setName("");
|
|
22
|
+
startTransition(async () => {
|
|
23
|
+
const res = await fetch("/api/fn/createWidget", {
|
|
24
|
+
method: "POST",
|
|
25
|
+
headers: { "Content-Type": "application/json" },
|
|
26
|
+
body: JSON.stringify({ name: trimmed }),
|
|
27
|
+
});
|
|
28
|
+
if (res.ok) {
|
|
29
|
+
const widget = (await res.json()) as Widget;
|
|
30
|
+
setWidgets((prev) => [widget, ...prev]);
|
|
31
|
+
}
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return (
|
|
36
|
+
<div className="space-y-4">
|
|
37
|
+
<form
|
|
38
|
+
onSubmit={(e) => {
|
|
39
|
+
e.preventDefault();
|
|
40
|
+
add();
|
|
41
|
+
}}
|
|
42
|
+
className="flex gap-2"
|
|
43
|
+
>
|
|
44
|
+
<Input
|
|
45
|
+
value={name}
|
|
46
|
+
onChange={(e) => setName(e.target.value)}
|
|
47
|
+
placeholder="Name a widget…"
|
|
48
|
+
disabled={pending}
|
|
49
|
+
className="flex-1"
|
|
50
|
+
/>
|
|
51
|
+
<Button
|
|
52
|
+
type="submit"
|
|
53
|
+
variant="primary"
|
|
54
|
+
disabled={pending || !name.trim()}
|
|
55
|
+
>
|
|
56
|
+
Create
|
|
57
|
+
</Button>
|
|
58
|
+
</form>
|
|
59
|
+
|
|
60
|
+
{widgets.length === 0 ? (
|
|
61
|
+
<p className="text-sm text-neutral-500 dark:text-neutral-400 text-center py-8">
|
|
62
|
+
No widgets yet. Create one above.
|
|
63
|
+
</p>
|
|
64
|
+
) : (
|
|
65
|
+
<ul className="divide-y divide-neutral-200 dark:divide-neutral-800 rounded-md border border-neutral-200 dark:border-neutral-800">
|
|
66
|
+
{widgets.map((w) => (
|
|
67
|
+
<li
|
|
68
|
+
key={w.id}
|
|
69
|
+
className="flex items-center justify-between gap-3 px-4 py-3 text-sm bg-white dark:bg-neutral-950"
|
|
70
|
+
>
|
|
71
|
+
<span className="font-medium">{w.name}</span>
|
|
72
|
+
<span className="font-mono text-xs text-neutral-400">
|
|
73
|
+
count: {w.count}
|
|
74
|
+
</span>
|
|
75
|
+
</li>
|
|
76
|
+
))}
|
|
77
|
+
</ul>
|
|
78
|
+
)}
|
|
79
|
+
</div>
|
|
80
|
+
);
|
|
81
|
+
}
|
|
@@ -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: "Realtime app 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,43 @@
|
|
|
1
|
+
import { pylon } from "@/lib/pylon";
|
|
2
|
+
import { WidgetList } from "./components/WidgetList";
|
|
3
|
+
|
|
4
|
+
// Force dynamic — every render reads the live widget list from Pylon.
|
|
5
|
+
export const dynamic = "force-dynamic";
|
|
6
|
+
|
|
7
|
+
type Widget = {
|
|
8
|
+
id: string;
|
|
9
|
+
name: string;
|
|
10
|
+
count: number;
|
|
11
|
+
createdAt: string;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export default async function HomePage() {
|
|
15
|
+
const widgets = await pylon
|
|
16
|
+
.json<Widget[]>("/api/fn/listWidgets", {
|
|
17
|
+
method: "POST",
|
|
18
|
+
body: "{}",
|
|
19
|
+
headers: { "Content-Type": "application/json" },
|
|
20
|
+
})
|
|
21
|
+
.catch(() => [] as Widget[]);
|
|
22
|
+
|
|
23
|
+
return (
|
|
24
|
+
<main className="mx-auto max-w-2xl px-6 py-12 space-y-8">
|
|
25
|
+
<header className="space-y-2">
|
|
26
|
+
<h1 className="text-3xl font-semibold tracking-tight">__APP_NAME__</h1>
|
|
27
|
+
<p className="text-sm text-neutral-500 dark:text-neutral-400">
|
|
28
|
+
A bare-bones Pylon app. Edit{" "}
|
|
29
|
+
<code className="font-mono text-xs">apps/api/schema.ts</code> to
|
|
30
|
+
change the data model,{" "}
|
|
31
|
+
<code className="font-mono text-xs">apps/api/functions/</code> to
|
|
32
|
+
add handlers, or{" "}
|
|
33
|
+
<code className="font-mono text-xs">
|
|
34
|
+
apps/web/src/app/components/WidgetList.tsx
|
|
35
|
+
</code>{" "}
|
|
36
|
+
for the UI.
|
|
37
|
+
</p>
|
|
38
|
+
</header>
|
|
39
|
+
|
|
40
|
+
<WidgetList initialWidgets={widgets} />
|
|
41
|
+
</main>
|
|
42
|
+
);
|
|
43
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { createPylonServer } from "@pylonsync/next/server";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Single server-helper instance. Imported by every Server Component
|
|
5
|
+
* and Server Action that needs to talk to the Pylon control plane.
|
|
6
|
+
*
|
|
7
|
+
* `cookieName` MUST match the backend's emitted cookie. Pylon uses
|
|
8
|
+
* `${app_name}_session` from the manifest — for this app that's
|
|
9
|
+
* `__APP_NAME_SNAKE___session`. Pin it in code (NOT env) so a bad
|
|
10
|
+
* deployment env can't silently break auth.
|
|
11
|
+
*/
|
|
12
|
+
export const pylon = createPylonServer({
|
|
13
|
+
cookieName: "__APP_NAME_SNAKE___session",
|
|
14
|
+
});
|
|
@@ -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,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
|
+
}
|