@pylonsync/create-pylon 0.3.279 → 0.3.281
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 +6 -13
- package/package.json +1 -1
- package/templates/backend/chat/apps/api/schema.ts +1 -1
- package/templates/default/app/auth-form.tsx +36 -13
- package/templates/default/app/dashboard/billing/page.tsx +6 -6
- package/templates/default/app/dashboard/dashboard-client.tsx +4 -4
- package/templates/default/app/dashboard/members/page.tsx +5 -3
- package/templates/default/app/dashboard/page.tsx +9 -6
- package/templates/default/app/dashboard/projects/page.tsx +5 -3
- package/templates/default/app/dashboard/provision-workspace.tsx +74 -0
- package/templates/default/app/dashboard/settings/page.tsx +5 -3
- package/templates/default/app/layout.tsx +1 -1
- package/templates/default/components/dashboard-shell.tsx +1 -0
- package/templates/b2b/AGENTS.md +0 -61
- package/templates/b2b/README.md +0 -62
- package/templates/b2b/app/auth-form.tsx +0 -142
- package/templates/b2b/app/dashboard/dashboard-client.tsx +0 -192
- package/templates/b2b/app/dashboard/page.tsx +0 -63
- package/templates/b2b/app/error.tsx +0 -43
- package/templates/b2b/app/globals.css +0 -139
- package/templates/b2b/app/layout.tsx +0 -71
- package/templates/b2b/app/login/page.tsx +0 -47
- package/templates/b2b/app/not-found.tsx +0 -29
- package/templates/b2b/app/page.tsx +0 -114
- package/templates/b2b/app/robots.ts +0 -12
- package/templates/b2b/app/signup/page.tsx +0 -44
- package/templates/b2b/app/sitemap.ts +0 -27
- package/templates/b2b/app.ts +0 -179
- package/templates/b2b/components/ui/button.tsx +0 -56
- package/templates/b2b/components/ui/card.tsx +0 -90
- package/templates/b2b/components.json +0 -20
- package/templates/b2b/functions/_keep.ts +0 -13
- package/templates/b2b/gitignore +0 -10
- package/templates/b2b/lib/utils.ts +0 -10
- package/templates/b2b/package.json +0 -33
- package/templates/b2b/tsconfig.json +0 -18
- package/templates/default/app/onboarding/onboarding-client.tsx +0 -261
- package/templates/default/app/onboarding/page.tsx +0 -29
|
@@ -1,192 +0,0 @@
|
|
|
1
|
-
"use client";
|
|
2
|
-
|
|
3
|
-
import React, { useCallback, useEffect, useState } from "react";
|
|
4
|
-
import { db } from "@pylonsync/react";
|
|
5
|
-
import {
|
|
6
|
-
useAuth,
|
|
7
|
-
OrganizationSwitcher,
|
|
8
|
-
listOrgMembers,
|
|
9
|
-
createInvite,
|
|
10
|
-
type OrgMember,
|
|
11
|
-
} from "@pylonsync/client";
|
|
12
|
-
import { Button } from "@/components/ui/button";
|
|
13
|
-
|
|
14
|
-
export interface Project {
|
|
15
|
-
id: string;
|
|
16
|
-
orgId: string;
|
|
17
|
-
name: string;
|
|
18
|
-
createdAt: string;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
// The workspace. `<OrganizationSwitcher>` (from @pylonsync/client) lists your
|
|
22
|
-
// orgs, creates new ones, and switches your active tenant via
|
|
23
|
-
// /api/auth/select-org — all against the framework's built-in org system. The
|
|
24
|
-
// rest of the page keys off `tenantId` (your active org).
|
|
25
|
-
export function Workspace() {
|
|
26
|
-
const { tenantId, signOut } = useAuth();
|
|
27
|
-
|
|
28
|
-
async function onSignOut() {
|
|
29
|
-
await signOut();
|
|
30
|
-
window.location.assign("/");
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
return (
|
|
34
|
-
<div className="space-y-6">
|
|
35
|
-
<div className="flex items-center justify-between gap-3">
|
|
36
|
-
<OrganizationSwitcher />
|
|
37
|
-
<Button variant="ghost" size="sm" onClick={onSignOut}>
|
|
38
|
-
Sign out
|
|
39
|
-
</Button>
|
|
40
|
-
</div>
|
|
41
|
-
|
|
42
|
-
{tenantId ? (
|
|
43
|
-
<div className="grid gap-6 sm:grid-cols-2">
|
|
44
|
-
<Projects orgId={tenantId} />
|
|
45
|
-
<Members orgId={tenantId} />
|
|
46
|
-
</div>
|
|
47
|
-
) : (
|
|
48
|
-
<div className="rounded-lg border border-dashed px-6 py-10 text-center text-sm text-muted-foreground">
|
|
49
|
-
Create or select an organization above to get started. Each org is an
|
|
50
|
-
isolated tenant — its projects and members are private to it.
|
|
51
|
-
</div>
|
|
52
|
-
)}
|
|
53
|
-
</div>
|
|
54
|
-
);
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
// Tenant-scoped data. `db.useQuery("Project")` returns only your active org's
|
|
58
|
-
// projects (the policy gates on `auth.tenantId == data.orgId`), and switching
|
|
59
|
-
// orgs re-syncs the list. `db.insert` is optimistic; we pass `orgId` = the
|
|
60
|
-
// active tenant so the row lands in this org — the policy rejects any other.
|
|
61
|
-
function Projects({ orgId }: { orgId: string }) {
|
|
62
|
-
const [name, setName] = useState("");
|
|
63
|
-
const { data: all } = db.useQuery<Project>("Project");
|
|
64
|
-
// Defensive filter while a tenant switch re-syncs the local replica.
|
|
65
|
-
const projects = all.filter((p) => p.orgId === orgId);
|
|
66
|
-
|
|
67
|
-
async function add(e: React.FormEvent) {
|
|
68
|
-
e.preventDefault();
|
|
69
|
-
const value = name.trim();
|
|
70
|
-
if (!value) return;
|
|
71
|
-
setName("");
|
|
72
|
-
await db.insert("Project", { orgId, name: value });
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
return (
|
|
76
|
-
<section className="space-y-3">
|
|
77
|
-
<h2 className="text-sm font-semibold">Projects</h2>
|
|
78
|
-
<form onSubmit={add} className="flex items-center gap-2">
|
|
79
|
-
<input
|
|
80
|
-
value={name}
|
|
81
|
-
onChange={(e) => setName(e.target.value)}
|
|
82
|
-
placeholder="New project…"
|
|
83
|
-
aria-label="Project name"
|
|
84
|
-
className="flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-sm shadow-sm outline-none focus-visible:ring-1 focus-visible:ring-ring"
|
|
85
|
-
/>
|
|
86
|
-
<Button type="submit" size="sm">
|
|
87
|
-
Add
|
|
88
|
-
</Button>
|
|
89
|
-
</form>
|
|
90
|
-
{projects.length === 0 ? (
|
|
91
|
-
<p className="text-sm text-muted-foreground">No projects yet.</p>
|
|
92
|
-
) : (
|
|
93
|
-
<ul className="space-y-1.5">
|
|
94
|
-
{projects.map((p) => (
|
|
95
|
-
<li
|
|
96
|
-
key={p.id}
|
|
97
|
-
className="flex items-center justify-between rounded-md border px-3 py-2 text-sm"
|
|
98
|
-
>
|
|
99
|
-
<span className="truncate">{p.name}</span>
|
|
100
|
-
<button
|
|
101
|
-
type="button"
|
|
102
|
-
aria-label="Delete project"
|
|
103
|
-
onClick={() => db.delete("Project", p.id)}
|
|
104
|
-
className="text-muted-foreground/40 transition-colors hover:text-red-600"
|
|
105
|
-
>
|
|
106
|
-
✕
|
|
107
|
-
</button>
|
|
108
|
-
</li>
|
|
109
|
-
))}
|
|
110
|
-
</ul>
|
|
111
|
-
)}
|
|
112
|
-
<p className="text-xs text-muted-foreground">
|
|
113
|
-
Tenant-scoped: only this org's projects, enforced by policy — switch
|
|
114
|
-
orgs and the list changes.
|
|
115
|
-
</p>
|
|
116
|
-
</section>
|
|
117
|
-
);
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
// Membership + invites go through the framework's /api/auth/orgs/:id endpoints
|
|
121
|
-
// (the @pylonsync/client helpers). The framework gates invites to org admins,
|
|
122
|
-
// so a member calling createInvite gets a 403 — real RBAC, no extra code.
|
|
123
|
-
function Members({ orgId }: { orgId: string }) {
|
|
124
|
-
const [members, setMembers] = useState<OrgMember[] | null>(null);
|
|
125
|
-
const [email, setEmail] = useState("");
|
|
126
|
-
const [note, setNote] = useState("");
|
|
127
|
-
|
|
128
|
-
const refresh = useCallback(() => {
|
|
129
|
-
void listOrgMembers(orgId).then(setMembers);
|
|
130
|
-
}, [orgId]);
|
|
131
|
-
|
|
132
|
-
useEffect(() => {
|
|
133
|
-
setMembers(null);
|
|
134
|
-
refresh();
|
|
135
|
-
}, [refresh]);
|
|
136
|
-
|
|
137
|
-
async function invite(e: React.FormEvent) {
|
|
138
|
-
e.preventDefault();
|
|
139
|
-
const value = email.trim();
|
|
140
|
-
if (!value) return;
|
|
141
|
-
setEmail("");
|
|
142
|
-
setNote("");
|
|
143
|
-
try {
|
|
144
|
-
await createInvite(orgId, value, "member");
|
|
145
|
-
setNote(`Invited ${value}.`);
|
|
146
|
-
refresh();
|
|
147
|
-
} catch {
|
|
148
|
-
setNote("Only org admins can invite members.");
|
|
149
|
-
}
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
return (
|
|
153
|
-
<section className="space-y-3">
|
|
154
|
-
<h2 className="text-sm font-semibold">Members</h2>
|
|
155
|
-
{members === null ? (
|
|
156
|
-
<p className="text-sm text-muted-foreground">Loading…</p>
|
|
157
|
-
) : (
|
|
158
|
-
<ul className="space-y-1.5">
|
|
159
|
-
{members.map((m) => (
|
|
160
|
-
<li
|
|
161
|
-
key={m.user_id}
|
|
162
|
-
className="flex items-center justify-between rounded-md border px-3 py-2 text-sm"
|
|
163
|
-
>
|
|
164
|
-
<span className="truncate font-mono text-xs">
|
|
165
|
-
{shortId(m.user_id)}
|
|
166
|
-
</span>
|
|
167
|
-
<span className="text-xs text-muted-foreground">{m.role}</span>
|
|
168
|
-
</li>
|
|
169
|
-
))}
|
|
170
|
-
</ul>
|
|
171
|
-
)}
|
|
172
|
-
<form onSubmit={invite} className="flex items-center gap-2">
|
|
173
|
-
<input
|
|
174
|
-
type="email"
|
|
175
|
-
value={email}
|
|
176
|
-
onChange={(e) => setEmail(e.target.value)}
|
|
177
|
-
placeholder="invite by email…"
|
|
178
|
-
aria-label="Invite email"
|
|
179
|
-
className="flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-sm shadow-sm outline-none focus-visible:ring-1 focus-visible:ring-ring"
|
|
180
|
-
/>
|
|
181
|
-
<Button type="submit" size="sm" variant="outline">
|
|
182
|
-
Invite
|
|
183
|
-
</Button>
|
|
184
|
-
</form>
|
|
185
|
-
{note && <p className="text-xs text-muted-foreground">{note}</p>}
|
|
186
|
-
</section>
|
|
187
|
-
);
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
function shortId(id: string) {
|
|
191
|
-
return id.replace(/^user_/, "").slice(0, 10);
|
|
192
|
-
}
|
|
@@ -1,63 +0,0 @@
|
|
|
1
|
-
import React, { Suspense, use } from "react";
|
|
2
|
-
import {
|
|
3
|
-
type Metadata,
|
|
4
|
-
type PageProps,
|
|
5
|
-
type ServerData,
|
|
6
|
-
} from "@pylonsync/react";
|
|
7
|
-
import { Workspace } from "./dashboard-client";
|
|
8
|
-
|
|
9
|
-
export const metadata: Metadata = {
|
|
10
|
-
title: "Dashboard — __APP_NAME__",
|
|
11
|
-
robots: "noindex",
|
|
12
|
-
};
|
|
13
|
-
|
|
14
|
-
interface User {
|
|
15
|
-
id: string;
|
|
16
|
-
email: string;
|
|
17
|
-
displayName?: string;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
// Reads the signed-in user DURING the server render (owner-scoped: only your
|
|
21
|
-
// own row). The org list + the active org's projects load client-side, since
|
|
22
|
-
// they depend on the session's selected tenant.
|
|
23
|
-
function Greeting({
|
|
24
|
-
serverData,
|
|
25
|
-
userId,
|
|
26
|
-
}: {
|
|
27
|
-
serverData: ServerData;
|
|
28
|
-
userId: string;
|
|
29
|
-
}) {
|
|
30
|
-
const user = use(serverData.get<User>("User", userId));
|
|
31
|
-
return (
|
|
32
|
-
<p className="text-sm text-muted-foreground">
|
|
33
|
-
Signed in as{" "}
|
|
34
|
-
<span className="font-medium text-foreground">
|
|
35
|
-
{user?.displayName || user?.email || "you"}
|
|
36
|
-
</span>
|
|
37
|
-
.
|
|
38
|
-
</p>
|
|
39
|
-
);
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
// `app/dashboard/page.tsx` → `/dashboard`.
|
|
43
|
-
export default function DashboardPage({
|
|
44
|
-
auth,
|
|
45
|
-
response,
|
|
46
|
-
serverData,
|
|
47
|
-
}: PageProps) {
|
|
48
|
-
// Server-side auth gate: anonymous requests get a 307 to /login before any
|
|
49
|
-
// HTML. Must fire in the synchronous shell render, not inside <Suspense> —
|
|
50
|
-
// and return immediately so nothing renders below the already-sent redirect.
|
|
51
|
-
if (!auth.user_id) {
|
|
52
|
-
response.redirect("/login");
|
|
53
|
-
return null;
|
|
54
|
-
}
|
|
55
|
-
return (
|
|
56
|
-
<div className="space-y-6">
|
|
57
|
-
<Suspense fallback={null}>
|
|
58
|
-
<Greeting serverData={serverData} userId={auth.user_id!} />
|
|
59
|
-
</Suspense>
|
|
60
|
-
<Workspace />
|
|
61
|
-
</div>
|
|
62
|
-
);
|
|
63
|
-
}
|
|
@@ -1,43 +0,0 @@
|
|
|
1
|
-
import React from "react";
|
|
2
|
-
import { type ErrorBoundaryProps } from "@pylonsync/react";
|
|
3
|
-
import { Button } from "@/components/ui/button";
|
|
4
|
-
|
|
5
|
-
// `app/error.tsx` → the error boundary for this segment. It catches a throw
|
|
6
|
-
// in any page/layout below it and renders at HTTP 500. It's HYDRATED, so
|
|
7
|
-
// this is a real interactive client component: `reset()` re-attempts the
|
|
8
|
-
// route, and useState/onClick work. The thrown error reaches the client as
|
|
9
|
-
// `{ message, digest }` only — the stack stays in the dev overlay
|
|
10
|
-
// (PYLON_DEV_MODE) and the server logs, never in the page.
|
|
11
|
-
export default function Error({ error, reset }: ErrorBoundaryProps) {
|
|
12
|
-
const [tries, setTries] = React.useState(0);
|
|
13
|
-
return (
|
|
14
|
-
<div className="space-y-6">
|
|
15
|
-
<section>
|
|
16
|
-
<h1 className="text-2xl font-semibold tracking-tight">
|
|
17
|
-
Something went wrong
|
|
18
|
-
</h1>
|
|
19
|
-
<p className="mt-2 text-muted-foreground">{error.message}</p>
|
|
20
|
-
{error.digest ? (
|
|
21
|
-
<p className="mt-1 text-xs text-muted-foreground/70">
|
|
22
|
-
Reference: <code>{error.digest}</code>
|
|
23
|
-
</p>
|
|
24
|
-
) : null}
|
|
25
|
-
</section>
|
|
26
|
-
<div className="flex items-center gap-3">
|
|
27
|
-
<Button
|
|
28
|
-
onClick={() => {
|
|
29
|
-
setTries((n) => n + 1);
|
|
30
|
-
reset();
|
|
31
|
-
}}
|
|
32
|
-
>
|
|
33
|
-
Try again
|
|
34
|
-
</Button>
|
|
35
|
-
{tries > 0 ? (
|
|
36
|
-
<span className="text-sm text-muted-foreground">
|
|
37
|
-
Retried {tries} {tries === 1 ? "time" : "times"}
|
|
38
|
-
</span>
|
|
39
|
-
) : null}
|
|
40
|
-
</div>
|
|
41
|
-
</div>
|
|
42
|
-
);
|
|
43
|
-
}
|
|
@@ -1,139 +0,0 @@
|
|
|
1
|
-
@import "tailwindcss";
|
|
2
|
-
@import "tw-animate-css";
|
|
3
|
-
|
|
4
|
-
/* Tailwind v4 scans these globs for class names. `components/` is here so
|
|
5
|
-
shadcn/ui component classes are seen — add more @source lines if you put
|
|
6
|
-
markup elsewhere. */
|
|
7
|
-
@source "../app/**/*.{tsx,ts,jsx,js}";
|
|
8
|
-
@source "../components/**/*.{tsx,ts,jsx,js}";
|
|
9
|
-
|
|
10
|
-
@custom-variant dark (&:where(.dark, .dark *));
|
|
11
|
-
|
|
12
|
-
/* shadcn/ui design tokens (new-york / zinc). Edit these to re-theme the
|
|
13
|
-
whole app; `npx shadcn@latest add <component>` drops new components that
|
|
14
|
-
consume the same variables. Toggle dark mode by putting `class="dark"`
|
|
15
|
-
on <html>. */
|
|
16
|
-
:root {
|
|
17
|
-
--radius: 0.625rem;
|
|
18
|
-
--background: oklch(1 0 0);
|
|
19
|
-
--foreground: oklch(0.141 0.005 285.823);
|
|
20
|
-
--card: oklch(1 0 0);
|
|
21
|
-
--card-foreground: oklch(0.141 0.005 285.823);
|
|
22
|
-
--popover: oklch(1 0 0);
|
|
23
|
-
--popover-foreground: oklch(0.141 0.005 285.823);
|
|
24
|
-
--primary: oklch(0.21 0.006 285.885);
|
|
25
|
-
--primary-foreground: oklch(0.985 0 0);
|
|
26
|
-
--secondary: oklch(0.967 0.001 286.375);
|
|
27
|
-
--secondary-foreground: oklch(0.21 0.006 285.885);
|
|
28
|
-
--muted: oklch(0.967 0.001 286.375);
|
|
29
|
-
--muted-foreground: oklch(0.552 0.016 285.938);
|
|
30
|
-
--accent: oklch(0.967 0.001 286.375);
|
|
31
|
-
--accent-foreground: oklch(0.21 0.006 285.885);
|
|
32
|
-
--destructive: oklch(0.577 0.245 27.325);
|
|
33
|
-
--border: oklch(0.92 0.004 286.32);
|
|
34
|
-
--input: oklch(0.92 0.004 286.32);
|
|
35
|
-
--ring: oklch(0.705 0.015 286.067);
|
|
36
|
-
--chart-1: oklch(0.646 0.222 41.116);
|
|
37
|
-
--chart-2: oklch(0.6 0.118 184.704);
|
|
38
|
-
--chart-3: oklch(0.398 0.07 227.392);
|
|
39
|
-
--chart-4: oklch(0.828 0.189 84.429);
|
|
40
|
-
--chart-5: oklch(0.769 0.188 70.08);
|
|
41
|
-
--sidebar: oklch(0.985 0 0);
|
|
42
|
-
--sidebar-foreground: oklch(0.141 0.005 285.823);
|
|
43
|
-
--sidebar-primary: oklch(0.21 0.006 285.885);
|
|
44
|
-
--sidebar-primary-foreground: oklch(0.985 0 0);
|
|
45
|
-
--sidebar-accent: oklch(0.967 0.001 286.375);
|
|
46
|
-
--sidebar-accent-foreground: oklch(0.21 0.006 285.885);
|
|
47
|
-
--sidebar-border: oklch(0.92 0.004 286.32);
|
|
48
|
-
--sidebar-ring: oklch(0.705 0.015 286.067);
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
.dark {
|
|
52
|
-
--background: oklch(0.141 0.005 285.823);
|
|
53
|
-
--foreground: oklch(0.985 0 0);
|
|
54
|
-
--card: oklch(0.21 0.006 285.885);
|
|
55
|
-
--card-foreground: oklch(0.985 0 0);
|
|
56
|
-
--popover: oklch(0.21 0.006 285.885);
|
|
57
|
-
--popover-foreground: oklch(0.985 0 0);
|
|
58
|
-
--primary: oklch(0.92 0.004 286.32);
|
|
59
|
-
--primary-foreground: oklch(0.21 0.006 285.885);
|
|
60
|
-
--secondary: oklch(0.274 0.006 286.033);
|
|
61
|
-
--secondary-foreground: oklch(0.985 0 0);
|
|
62
|
-
--muted: oklch(0.274 0.006 286.033);
|
|
63
|
-
--muted-foreground: oklch(0.705 0.015 286.067);
|
|
64
|
-
--accent: oklch(0.274 0.006 286.033);
|
|
65
|
-
--accent-foreground: oklch(0.985 0 0);
|
|
66
|
-
--destructive: oklch(0.704 0.191 22.216);
|
|
67
|
-
--border: oklch(1 0 0 / 10%);
|
|
68
|
-
--input: oklch(1 0 0 / 15%);
|
|
69
|
-
--ring: oklch(0.552 0.016 285.938);
|
|
70
|
-
--chart-1: oklch(0.488 0.243 264.376);
|
|
71
|
-
--chart-2: oklch(0.696 0.17 162.48);
|
|
72
|
-
--chart-3: oklch(0.769 0.188 70.08);
|
|
73
|
-
--chart-4: oklch(0.627 0.265 303.9);
|
|
74
|
-
--chart-5: oklch(0.645 0.246 16.439);
|
|
75
|
-
--sidebar: oklch(0.21 0.006 285.885);
|
|
76
|
-
--sidebar-foreground: oklch(0.985 0 0);
|
|
77
|
-
--sidebar-primary: oklch(0.488 0.243 264.376);
|
|
78
|
-
--sidebar-primary-foreground: oklch(0.985 0 0);
|
|
79
|
-
--sidebar-accent: oklch(0.274 0.006 286.033);
|
|
80
|
-
--sidebar-accent-foreground: oklch(0.985 0 0);
|
|
81
|
-
--sidebar-border: oklch(1 0 0 / 10%);
|
|
82
|
-
--sidebar-ring: oklch(0.552 0.016 285.938);
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
@theme inline {
|
|
86
|
-
--radius-sm: calc(var(--radius) - 4px);
|
|
87
|
-
--radius-md: calc(var(--radius) - 2px);
|
|
88
|
-
--radius-lg: var(--radius);
|
|
89
|
-
--radius-xl: calc(var(--radius) + 4px);
|
|
90
|
-
--color-background: var(--background);
|
|
91
|
-
--color-foreground: var(--foreground);
|
|
92
|
-
--color-card: var(--card);
|
|
93
|
-
--color-card-foreground: var(--card-foreground);
|
|
94
|
-
--color-popover: var(--popover);
|
|
95
|
-
--color-popover-foreground: var(--popover-foreground);
|
|
96
|
-
--color-primary: var(--primary);
|
|
97
|
-
--color-primary-foreground: var(--primary-foreground);
|
|
98
|
-
--color-secondary: var(--secondary);
|
|
99
|
-
--color-secondary-foreground: var(--secondary-foreground);
|
|
100
|
-
--color-muted: var(--muted);
|
|
101
|
-
--color-muted-foreground: var(--muted-foreground);
|
|
102
|
-
--color-accent: var(--accent);
|
|
103
|
-
--color-accent-foreground: var(--accent-foreground);
|
|
104
|
-
--color-destructive: var(--destructive);
|
|
105
|
-
--color-border: var(--border);
|
|
106
|
-
--color-input: var(--input);
|
|
107
|
-
--color-ring: var(--ring);
|
|
108
|
-
--color-chart-1: var(--chart-1);
|
|
109
|
-
--color-chart-2: var(--chart-2);
|
|
110
|
-
--color-chart-3: var(--chart-3);
|
|
111
|
-
--color-chart-4: var(--chart-4);
|
|
112
|
-
--color-chart-5: var(--chart-5);
|
|
113
|
-
--color-sidebar: var(--sidebar);
|
|
114
|
-
--color-sidebar-foreground: var(--sidebar-foreground);
|
|
115
|
-
--color-sidebar-primary: var(--sidebar-primary);
|
|
116
|
-
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
|
|
117
|
-
--color-sidebar-accent: var(--sidebar-accent);
|
|
118
|
-
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
|
|
119
|
-
--color-sidebar-border: var(--sidebar-border);
|
|
120
|
-
--color-sidebar-ring: var(--sidebar-ring);
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
@layer base {
|
|
124
|
-
*,
|
|
125
|
-
::after,
|
|
126
|
-
::before,
|
|
127
|
-
::backdrop,
|
|
128
|
-
::file-selector-button {
|
|
129
|
-
border-color: var(--color-border, currentColor);
|
|
130
|
-
outline-color: var(--color-ring);
|
|
131
|
-
}
|
|
132
|
-
body {
|
|
133
|
-
background-color: var(--color-background);
|
|
134
|
-
color: var(--color-foreground);
|
|
135
|
-
}
|
|
136
|
-
button {
|
|
137
|
-
cursor: pointer;
|
|
138
|
-
}
|
|
139
|
-
}
|
|
@@ -1,71 +0,0 @@
|
|
|
1
|
-
import React from "react";
|
|
2
|
-
import { Link, type PageAuth } from "@pylonsync/react";
|
|
3
|
-
|
|
4
|
-
// A layout receives the page props plus `children`. `auth.user_id` is null
|
|
5
|
-
// for anonymous visitors and the signed-in user's id otherwise — resolved
|
|
6
|
-
// server-side from the session cookie, before any HTML is sent, so the nav
|
|
7
|
-
// renders the right links on the first byte (no flash, no client fetch). The
|
|
8
|
-
// `PageAuth` type is exported from @pylonsync/react so you never hand-roll it.
|
|
9
|
-
interface LayoutProps {
|
|
10
|
-
children: React.ReactNode;
|
|
11
|
-
url: string;
|
|
12
|
-
auth: PageAuth;
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
// The root layout wraps every page.
|
|
16
|
-
export default function RootLayout({ children, auth }: LayoutProps) {
|
|
17
|
-
const signedIn = Boolean(auth?.user_id);
|
|
18
|
-
// Add `className="dark"` to this <html> to flip every shadcn token to its
|
|
19
|
-
// dark value. The classes below use semantic tokens (bg-background,
|
|
20
|
-
// text-foreground, …) so the whole UI re-themes from app/globals.css.
|
|
21
|
-
return (
|
|
22
|
-
<html lang="en">
|
|
23
|
-
<head>
|
|
24
|
-
<meta charSet="utf-8" />
|
|
25
|
-
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
26
|
-
<title>__APP_NAME__</title>
|
|
27
|
-
{/* Tailwind is compiled by Pylon from app/globals.css and the
|
|
28
|
-
stylesheet link is injected here automatically — nothing to
|
|
29
|
-
wire up. */}
|
|
30
|
-
</head>
|
|
31
|
-
<body className="min-h-screen bg-background text-foreground antialiased">
|
|
32
|
-
<header className="sticky top-0 z-10 border-b bg-background/80 backdrop-blur">
|
|
33
|
-
<div className="mx-auto flex max-w-3xl items-center justify-between px-4 py-3">
|
|
34
|
-
<Link
|
|
35
|
-
href="/"
|
|
36
|
-
className="text-sm font-semibold tracking-tight hover:text-muted-foreground"
|
|
37
|
-
>
|
|
38
|
-
__APP_NAME__
|
|
39
|
-
</Link>
|
|
40
|
-
<nav className="flex items-center gap-4 text-sm text-muted-foreground">
|
|
41
|
-
<Link href="/" className="hover:text-foreground">
|
|
42
|
-
Home
|
|
43
|
-
</Link>
|
|
44
|
-
{signedIn ? (
|
|
45
|
-
<Link href="/dashboard" className="hover:text-foreground">
|
|
46
|
-
Dashboard
|
|
47
|
-
</Link>
|
|
48
|
-
) : (
|
|
49
|
-
<>
|
|
50
|
-
<Link href="/login" className="hover:text-foreground">
|
|
51
|
-
Sign in
|
|
52
|
-
</Link>
|
|
53
|
-
<Link
|
|
54
|
-
href="/signup"
|
|
55
|
-
className="rounded-md bg-primary px-3 py-1.5 font-medium text-primary-foreground hover:opacity-90"
|
|
56
|
-
>
|
|
57
|
-
Sign up
|
|
58
|
-
</Link>
|
|
59
|
-
</>
|
|
60
|
-
)}
|
|
61
|
-
</nav>
|
|
62
|
-
</div>
|
|
63
|
-
</header>
|
|
64
|
-
<main className="mx-auto max-w-3xl px-4 py-10">{children}</main>
|
|
65
|
-
<footer className="border-t py-6 text-center text-xs text-muted-foreground">
|
|
66
|
-
Rendered by Pylon
|
|
67
|
-
</footer>
|
|
68
|
-
</body>
|
|
69
|
-
</html>
|
|
70
|
-
);
|
|
71
|
-
}
|
|
@@ -1,47 +0,0 @@
|
|
|
1
|
-
import React from "react";
|
|
2
|
-
import { Link, type Metadata, type PageProps } from "@pylonsync/react";
|
|
3
|
-
import {
|
|
4
|
-
Card,
|
|
5
|
-
CardContent,
|
|
6
|
-
CardDescription,
|
|
7
|
-
CardHeader,
|
|
8
|
-
CardTitle,
|
|
9
|
-
} from "@/components/ui/card";
|
|
10
|
-
import { AuthForm } from "../auth-form";
|
|
11
|
-
|
|
12
|
-
export const metadata: Metadata = {
|
|
13
|
-
title: "Sign in — __APP_NAME__",
|
|
14
|
-
// Auth pages shouldn't be indexed.
|
|
15
|
-
robots: "noindex",
|
|
16
|
-
};
|
|
17
|
-
|
|
18
|
-
// `app/login/page.tsx` → `/login`. A server-rendered shell around the
|
|
19
|
-
// client-side <AuthForm> island.
|
|
20
|
-
export default function LoginPage({ auth, response }: PageProps) {
|
|
21
|
-
// Already signed in? Skip the form. `response.redirect` runs in the
|
|
22
|
-
// synchronous shell render, so it's a real 307 before any HTML is sent
|
|
23
|
-
// (no flash, works with JS disabled).
|
|
24
|
-
if (auth.user_id) response.redirect("/dashboard");
|
|
25
|
-
return (
|
|
26
|
-
<div className="mx-auto max-w-sm">
|
|
27
|
-
<Card>
|
|
28
|
-
<CardHeader>
|
|
29
|
-
<CardTitle>Sign in</CardTitle>
|
|
30
|
-
<CardDescription>Welcome back.</CardDescription>
|
|
31
|
-
</CardHeader>
|
|
32
|
-
<CardContent className="space-y-4">
|
|
33
|
-
<AuthForm mode="login" />
|
|
34
|
-
<p className="text-center text-sm text-muted-foreground">
|
|
35
|
-
No account?{" "}
|
|
36
|
-
<Link
|
|
37
|
-
href="/signup"
|
|
38
|
-
className="font-medium text-foreground hover:underline"
|
|
39
|
-
>
|
|
40
|
-
Create one
|
|
41
|
-
</Link>
|
|
42
|
-
</p>
|
|
43
|
-
</CardContent>
|
|
44
|
-
</Card>
|
|
45
|
-
</div>
|
|
46
|
-
);
|
|
47
|
-
}
|
|
@@ -1,29 +0,0 @@
|
|
|
1
|
-
import React from "react";
|
|
2
|
-
import { Link, useRouter, type NotFoundProps } from "@pylonsync/react";
|
|
3
|
-
import { Button } from "@/components/ui/button";
|
|
4
|
-
|
|
5
|
-
// `app/not-found.tsx` → rendered at HTTP 404 for any unmatched URL (and when
|
|
6
|
-
// a page calls `response.notFound()`). It's HYDRATED, so it's interactive:
|
|
7
|
-
// the buttons below use the client router. Not-found boundaries receive the
|
|
8
|
-
// standard page props (and, matching Next, no `reset`).
|
|
9
|
-
export default function NotFound(_props: NotFoundProps) {
|
|
10
|
-
const router = useRouter();
|
|
11
|
-
return (
|
|
12
|
-
<div className="space-y-6">
|
|
13
|
-
<section>
|
|
14
|
-
<h1 className="text-2xl font-semibold tracking-tight">404</h1>
|
|
15
|
-
<p className="mt-2 text-muted-foreground">
|
|
16
|
-
We couldn't find that page.
|
|
17
|
-
</p>
|
|
18
|
-
</section>
|
|
19
|
-
<div className="flex items-center gap-3">
|
|
20
|
-
<Button onClick={() => router.back()} variant="outline">
|
|
21
|
-
← Go back
|
|
22
|
-
</Button>
|
|
23
|
-
<Button asChild>
|
|
24
|
-
<Link href="/">Home</Link>
|
|
25
|
-
</Button>
|
|
26
|
-
</div>
|
|
27
|
-
</div>
|
|
28
|
-
);
|
|
29
|
-
}
|