@pylonsync/create-pylon 0.3.374 → 0.3.375
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/package.json +1 -1
- package/templates/saas/app/{auth-shell.tsx → (auth)/layout.tsx} +21 -29
- package/templates/saas/app/(auth)/login/page.tsx +33 -0
- package/templates/saas/app/(auth)/signup/page.tsx +32 -0
- package/templates/saas/app/(marketing)/layout.tsx +2 -2
- package/templates/saas/app/dashboard/billing/page.tsx +7 -20
- package/templates/saas/app/dashboard/layout.tsx +58 -0
- package/templates/saas/app/dashboard/members/page.tsx +9 -22
- package/templates/saas/app/dashboard/page.tsx +15 -32
- package/templates/saas/app/dashboard/projects/page.tsx +4 -23
- package/templates/saas/app/dashboard/settings/page.tsx +7 -19
- package/templates/saas/app/layout.tsx +4 -5
- package/templates/saas/components/dashboard-shell.tsx +10 -6
- package/templates/saas/app/login/page.tsx +0 -28
- package/templates/saas/app/signup/page.tsx +0 -24
- /package/templates/saas/app/{auth-form.tsx → (auth)/auth-form.tsx} +0 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pylonsync/create-pylon",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.375",
|
|
4
4
|
"description": "Scaffold a new Pylon app — realtime backend + web/mobile/expo frontends in one command. Run via `npm create @pylonsync/pylon@latest`.",
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public"
|
|
@@ -1,22 +1,26 @@
|
|
|
1
1
|
import React from "react";
|
|
2
|
-
import { Link } from "@pylonsync/react";
|
|
2
|
+
import { Link, type PageAuth, type SsrResponse } from "@pylonsync/react";
|
|
3
3
|
|
|
4
|
-
//
|
|
5
|
-
// left, a brand/testimonial panel on the right (hidden on
|
|
6
|
-
//
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
switchHref,
|
|
12
|
-
children,
|
|
13
|
-
}: {
|
|
14
|
-
title: string;
|
|
15
|
-
switchPrompt: string;
|
|
16
|
-
switchLabel: string;
|
|
17
|
-
switchHref: string;
|
|
4
|
+
// `(auth)` route group → the shared split-screen frame for /login and /signup:
|
|
5
|
+
// the form card on the left, a brand/testimonial panel on the right (hidden on
|
|
6
|
+
// small screens). Each page supplies what differs — heading, switch link, and
|
|
7
|
+
// the form — as `children`. The group segment adds no URL prefix, and because
|
|
8
|
+
// these routes sit outside `(marketing)`, none of the site nav/footer renders
|
|
9
|
+
// here.
|
|
10
|
+
interface LayoutProps {
|
|
18
11
|
children: React.ReactNode;
|
|
19
|
-
|
|
12
|
+
auth: PageAuth;
|
|
13
|
+
response: SsrResponse;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export default function AuthLayout({ children, auth, response }: LayoutProps) {
|
|
17
|
+
// Already signed in? Skip the auth screens entirely. Gating here covers
|
|
18
|
+
// every page in the group; `response.redirect` runs in the synchronous
|
|
19
|
+
// shell render, so it's a real 307 before any HTML is sent.
|
|
20
|
+
if (auth.user_id) {
|
|
21
|
+
response.redirect("/dashboard");
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
20
24
|
return (
|
|
21
25
|
<div className="grid min-h-screen bg-white lg:grid-cols-2">
|
|
22
26
|
{/* Form side */}
|
|
@@ -27,19 +31,7 @@ export function AuthShell({
|
|
|
27
31
|
A
|
|
28
32
|
</span>
|
|
29
33
|
</Link>
|
|
30
|
-
|
|
31
|
-
{title}
|
|
32
|
-
</h1>
|
|
33
|
-
<p className="mt-1 text-[13px] text-zinc-500">
|
|
34
|
-
{switchPrompt}{" "}
|
|
35
|
-
<Link
|
|
36
|
-
href={switchHref}
|
|
37
|
-
className="font-medium text-zinc-900 underline underline-offset-2"
|
|
38
|
-
>
|
|
39
|
-
{switchLabel}
|
|
40
|
-
</Link>
|
|
41
|
-
</p>
|
|
42
|
-
<div className="mt-6">{children}</div>
|
|
34
|
+
{children}
|
|
43
35
|
</div>
|
|
44
36
|
</div>
|
|
45
37
|
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { Link, type Metadata } from "@pylonsync/react";
|
|
3
|
+
import { AuthForm } from "../auth-form";
|
|
4
|
+
|
|
5
|
+
export const metadata: Metadata = {
|
|
6
|
+
title: "Sign in — Acme",
|
|
7
|
+
robots: "noindex",
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
// `(auth)/login/page.tsx` → `/login`. The `(auth)` layout owns the
|
|
11
|
+
// split-screen frame and the already-signed-in redirect; this page is just
|
|
12
|
+
// the heading, the switch link, and the form.
|
|
13
|
+
export default function LoginPage() {
|
|
14
|
+
return (
|
|
15
|
+
<>
|
|
16
|
+
<h1 className="mt-5 text-[22px] font-semibold tracking-tight text-zinc-900">
|
|
17
|
+
Welcome back
|
|
18
|
+
</h1>
|
|
19
|
+
<p className="mt-1 text-[13px] text-zinc-500">
|
|
20
|
+
New to Acme?{" "}
|
|
21
|
+
<Link
|
|
22
|
+
href="/signup"
|
|
23
|
+
className="font-medium text-zinc-900 underline underline-offset-2"
|
|
24
|
+
>
|
|
25
|
+
Create an account
|
|
26
|
+
</Link>
|
|
27
|
+
</p>
|
|
28
|
+
<div className="mt-6">
|
|
29
|
+
<AuthForm mode="login" />
|
|
30
|
+
</div>
|
|
31
|
+
</>
|
|
32
|
+
);
|
|
33
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { Link, type Metadata } from "@pylonsync/react";
|
|
3
|
+
import { AuthForm } from "../auth-form";
|
|
4
|
+
|
|
5
|
+
export const metadata: Metadata = {
|
|
6
|
+
title: "Create your account — Acme",
|
|
7
|
+
robots: "noindex",
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
// `(auth)/signup/page.tsx` → `/signup`. Register mode of the same form; the
|
|
11
|
+
// `(auth)` layout supplies the frame and the signed-in redirect.
|
|
12
|
+
export default function SignupPage() {
|
|
13
|
+
return (
|
|
14
|
+
<>
|
|
15
|
+
<h1 className="mt-5 text-[22px] font-semibold tracking-tight text-zinc-900">
|
|
16
|
+
Create an account
|
|
17
|
+
</h1>
|
|
18
|
+
<p className="mt-1 text-[13px] text-zinc-500">
|
|
19
|
+
Already have an account?{" "}
|
|
20
|
+
<Link
|
|
21
|
+
href="/login"
|
|
22
|
+
className="font-medium text-zinc-900 underline underline-offset-2"
|
|
23
|
+
>
|
|
24
|
+
Sign in
|
|
25
|
+
</Link>
|
|
26
|
+
</p>
|
|
27
|
+
<div className="mt-6">
|
|
28
|
+
<AuthForm mode="signup" />
|
|
29
|
+
</div>
|
|
30
|
+
</>
|
|
31
|
+
);
|
|
32
|
+
}
|
|
@@ -7,8 +7,8 @@ import { siteConfig } from "@/lib/site.config";
|
|
|
7
7
|
// `(marketing)` is a ROUTE GROUP: the parens segment is stripped from every
|
|
8
8
|
// URL (so `(marketing)/page.tsx` still serves `/`), and this layout wraps
|
|
9
9
|
// only the pages inside the group — the marketing nav up top, a fat footer
|
|
10
|
-
// below.
|
|
11
|
-
//
|
|
10
|
+
// below. The other sections bring their own layouts: `(auth)` the split-screen
|
|
11
|
+
// frame, `dashboard/` the sidebar shell.
|
|
12
12
|
//
|
|
13
13
|
// A layout receives the page props plus `children`. `auth.user_id` is null for
|
|
14
14
|
// anonymous visitors and the signed-in user's id otherwise — resolved
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import React, { use } from "react";
|
|
2
2
|
import { type Metadata, type PageProps } from "@pylonsync/react";
|
|
3
|
-
import { DashboardShell } from "@/components/dashboard-shell";
|
|
4
3
|
import { Billing, type Subscription } from "../dashboard-client";
|
|
5
4
|
|
|
6
5
|
export const metadata: Metadata = {
|
|
@@ -11,20 +10,15 @@ export const metadata: Metadata = {
|
|
|
11
10
|
// `/dashboard/billing` — the active workspace's plan + Stripe checkout/portal.
|
|
12
11
|
// The StripeSubscription row is resolved server-side (the @pylonsync/stripe
|
|
13
12
|
// read policy scopes it to the active tenant), so the plan paints with no flash;
|
|
14
|
-
// upgrade/manage open Stripe and the webhook keeps the row in sync.
|
|
13
|
+
// upgrade/manage open Stripe and the webhook keeps the row in sync. Auth gate +
|
|
14
|
+
// shell chrome come from the dashboard layout.
|
|
15
15
|
const ACTIVE = ["active", "trialing", "past_due"];
|
|
16
16
|
|
|
17
17
|
export default function BillingPage({ auth, response, serverData }: PageProps) {
|
|
18
|
-
if (!auth.user_id) {
|
|
19
|
-
response.redirect("/login");
|
|
20
|
-
return null;
|
|
21
|
-
}
|
|
22
18
|
if (!auth.tenant_id) {
|
|
23
19
|
response.redirect("/dashboard");
|
|
24
20
|
return null;
|
|
25
21
|
}
|
|
26
|
-
const me = use(serverData.get<{ email?: string }>("User", auth.user_id));
|
|
27
|
-
const org = use(serverData.get<{ name?: string }>("Org", auth.tenant_id));
|
|
28
22
|
const subs = use(serverData.list<Subscription>("StripeSubscription"));
|
|
29
23
|
const subscription =
|
|
30
24
|
subs.find(
|
|
@@ -33,17 +27,10 @@ export default function BillingPage({ auth, response, serverData }: PageProps) {
|
|
|
33
27
|
subs[0] ??
|
|
34
28
|
null;
|
|
35
29
|
return (
|
|
36
|
-
<
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
>
|
|
42
|
-
<Billing
|
|
43
|
-
tenantId={auth.tenant_id}
|
|
44
|
-
role={auth.roles?.[0] ?? ""}
|
|
45
|
-
subscription={subscription}
|
|
46
|
-
/>
|
|
47
|
-
</DashboardShell>
|
|
30
|
+
<Billing
|
|
31
|
+
tenantId={auth.tenant_id}
|
|
32
|
+
role={auth.roles?.[0] ?? ""}
|
|
33
|
+
subscription={subscription}
|
|
34
|
+
/>
|
|
48
35
|
);
|
|
49
36
|
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import React, { use } from "react";
|
|
2
|
+
import type { PageAuth, ServerData, SsrResponse } from "@pylonsync/react";
|
|
3
|
+
import { DashboardShell, NAV } from "@/components/dashboard-shell";
|
|
4
|
+
|
|
5
|
+
// `app/dashboard/layout.tsx` wraps every /dashboard page in the sidebar +
|
|
6
|
+
// top-bar shell, so no page repeats the chrome. This is the thin-server-
|
|
7
|
+
// wrapper pattern: the layout gates auth, resolves the chrome's data
|
|
8
|
+
// server-side (user email, org name), derives the active nav item + title
|
|
9
|
+
// from the request URL, and hands the interactive shell (a `"use client"`
|
|
10
|
+
// component) its props.
|
|
11
|
+
interface LayoutProps {
|
|
12
|
+
children: React.ReactNode;
|
|
13
|
+
url: string;
|
|
14
|
+
auth: PageAuth;
|
|
15
|
+
response: SsrResponse;
|
|
16
|
+
serverData: ServerData;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export default function DashboardLayout({
|
|
20
|
+
children,
|
|
21
|
+
url,
|
|
22
|
+
auth,
|
|
23
|
+
response,
|
|
24
|
+
serverData,
|
|
25
|
+
}: LayoutProps) {
|
|
26
|
+
// The layout renders before any page below it, so gating here protects the
|
|
27
|
+
// whole /dashboard section: signed-out visitors get a real 307 to /login
|
|
28
|
+
// and no page code runs.
|
|
29
|
+
if (!auth.user_id) {
|
|
30
|
+
response.redirect("/login");
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
// No active workspace: render the page bare — /dashboard shows the
|
|
34
|
+
// full-screen ProvisionWorkspace flow, and the subpages redirect back to
|
|
35
|
+
// /dashboard themselves. The shell's org switcher has nothing to show yet.
|
|
36
|
+
if (!auth.tenant_id) {
|
|
37
|
+
return <>{children}</>;
|
|
38
|
+
}
|
|
39
|
+
const me = use(serverData.get<{ email?: string }>("User", auth.user_id));
|
|
40
|
+
const org = use(serverData.get<{ name?: string }>("Org", auth.tenant_id));
|
|
41
|
+
// Longest matching NAV href wins so /dashboard/projects highlights
|
|
42
|
+
// Projects, not Overview.
|
|
43
|
+
const path = (url ?? "").split("?")[0];
|
|
44
|
+
const nav =
|
|
45
|
+
NAV.filter((n) => path === n.href || path.startsWith(n.href + "/")).sort(
|
|
46
|
+
(a, b) => b.href.length - a.href.length,
|
|
47
|
+
)[0] ?? NAV[0];
|
|
48
|
+
return (
|
|
49
|
+
<DashboardShell
|
|
50
|
+
active={nav.key}
|
|
51
|
+
title={nav.label}
|
|
52
|
+
userEmail={me?.email ?? ""}
|
|
53
|
+
orgName={org?.name}
|
|
54
|
+
>
|
|
55
|
+
{children}
|
|
56
|
+
</DashboardShell>
|
|
57
|
+
);
|
|
58
|
+
}
|
|
@@ -1,6 +1,5 @@
|
|
|
1
|
-
import React
|
|
1
|
+
import React from "react";
|
|
2
2
|
import { type Metadata, type PageProps } from "@pylonsync/react";
|
|
3
|
-
import { DashboardShell } from "@/components/dashboard-shell";
|
|
4
3
|
import { Members } from "../dashboard-client";
|
|
5
4
|
|
|
6
5
|
export const metadata: Metadata = {
|
|
@@ -10,30 +9,18 @@ export const metadata: Metadata = {
|
|
|
10
9
|
|
|
11
10
|
// `/dashboard/members` — the active org's roster + invites. The roster (with
|
|
12
11
|
// real emails) is loaded client-side from the framework's org-members endpoint;
|
|
13
|
-
// invites are gated to owners/admins both here and on the server.
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
response.redirect("/login");
|
|
17
|
-
return null;
|
|
18
|
-
}
|
|
12
|
+
// invites are gated to owners/admins both here and on the server. Auth gate +
|
|
13
|
+
// shell chrome come from the dashboard layout.
|
|
14
|
+
export default function MembersPage({ auth, response }: PageProps) {
|
|
19
15
|
if (!auth.tenant_id) {
|
|
20
16
|
response.redirect("/dashboard");
|
|
21
17
|
return null;
|
|
22
18
|
}
|
|
23
|
-
const me = use(serverData.get<{ email?: string }>("User", auth.user_id));
|
|
24
|
-
const org = use(serverData.get<{ name?: string }>("Org", auth.tenant_id));
|
|
25
19
|
return (
|
|
26
|
-
<
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
>
|
|
32
|
-
<Members
|
|
33
|
-
tenantId={auth.tenant_id}
|
|
34
|
-
currentUserId={auth.user_id}
|
|
35
|
-
role={auth.roles?.[0] ?? ""}
|
|
36
|
-
/>
|
|
37
|
-
</DashboardShell>
|
|
20
|
+
<Members
|
|
21
|
+
tenantId={auth.tenant_id}
|
|
22
|
+
currentUserId={auth.user_id!}
|
|
23
|
+
role={auth.roles?.[0] ?? ""}
|
|
24
|
+
/>
|
|
38
25
|
);
|
|
39
26
|
}
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import React, { use } from "react";
|
|
2
2
|
import { type Metadata, type PageProps } from "@pylonsync/react";
|
|
3
|
-
import { DashboardShell } from "@/components/dashboard-shell";
|
|
4
3
|
import { ProvisionWorkspace } from "./provision-workspace";
|
|
5
4
|
import {
|
|
6
5
|
Overview,
|
|
@@ -14,29 +13,20 @@ export const metadata: Metadata = {
|
|
|
14
13
|
robots: "noindex",
|
|
15
14
|
};
|
|
16
15
|
|
|
17
|
-
// `app/dashboard/page.tsx` → `/dashboard`.
|
|
18
|
-
//
|
|
19
|
-
// via `serverData` + React 19 `use()` — resolved server-side and
|
|
20
|
-
// hydration, so the dashboard paints with real data on the first
|
|
21
|
-
// client fetch, no empty-state flash).
|
|
22
|
-
|
|
23
|
-
// the shell is the only chrome.
|
|
24
|
-
export default function DashboardPage({
|
|
25
|
-
auth,
|
|
26
|
-
response,
|
|
27
|
-
serverData,
|
|
28
|
-
}: PageProps) {
|
|
29
|
-
if (!auth.user_id) {
|
|
30
|
-
response.redirect("/login");
|
|
31
|
-
return null;
|
|
32
|
-
}
|
|
16
|
+
// `app/dashboard/page.tsx` → `/dashboard`. The dashboard layout owns the auth
|
|
17
|
+
// gate and the shell chrome; this page reads the active org's rows during the
|
|
18
|
+
// render via `serverData` + React 19 `use()` — resolved server-side and
|
|
19
|
+
// replayed on hydration, so the dashboard paints with real data on the first
|
|
20
|
+
// byte (no client fetch, no empty-state flash).
|
|
21
|
+
export default function DashboardPage({ auth, serverData }: PageProps) {
|
|
33
22
|
// No active workspace (signup's auto-provision failed, or the user left/
|
|
34
23
|
// deleted their last org). Every read below is tenant-scoped, so instead of
|
|
35
24
|
// an empty shell, provision one client-side and reload into a ready dashboard.
|
|
25
|
+
// The layout renders this bare (no shell) when there's no tenant.
|
|
36
26
|
if (!auth.tenant_id) {
|
|
37
27
|
return <ProvisionWorkspace />;
|
|
38
28
|
}
|
|
39
|
-
const me = use(serverData.get<{ email?: string }>("User", auth.user_id));
|
|
29
|
+
const me = use(serverData.get<{ email?: string }>("User", auth.user_id!));
|
|
40
30
|
const org = use(serverData.get<{ name?: string }>("Org", auth.tenant_id));
|
|
41
31
|
const projects = use(serverData.list<Project>("Project"));
|
|
42
32
|
const members = use(serverData.list<OrgMemberRow>("OrgMember"));
|
|
@@ -51,20 +41,13 @@ export default function DashboardPage({
|
|
|
51
41
|
);
|
|
52
42
|
const plan = active ? active.plan : "free";
|
|
53
43
|
return (
|
|
54
|
-
<
|
|
55
|
-
|
|
56
|
-
title="Overview"
|
|
57
|
-
userEmail={me?.email ?? ""}
|
|
44
|
+
<Overview
|
|
45
|
+
tenantId={auth.tenant_id}
|
|
58
46
|
orgName={org?.name}
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
projects={projects}
|
|
65
|
-
memberCount={memberCount}
|
|
66
|
-
plan={plan}
|
|
67
|
-
/>
|
|
68
|
-
</DashboardShell>
|
|
47
|
+
userEmail={me?.email}
|
|
48
|
+
projects={projects}
|
|
49
|
+
memberCount={memberCount}
|
|
50
|
+
plan={plan}
|
|
51
|
+
/>
|
|
69
52
|
);
|
|
70
53
|
}
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import React, { use } from "react";
|
|
2
2
|
import { type Metadata, type PageProps } from "@pylonsync/react";
|
|
3
|
-
import { DashboardShell } from "@/components/dashboard-shell";
|
|
4
3
|
import { Projects, type Project } from "../dashboard-client";
|
|
5
4
|
|
|
6
5
|
export const metadata: Metadata = {
|
|
@@ -9,31 +8,13 @@ export const metadata: Metadata = {
|
|
|
9
8
|
};
|
|
10
9
|
|
|
11
10
|
// `/dashboard/projects` — this org's projects, server-resolved via `use()` then
|
|
12
|
-
// kept live + optimistic by `db` on the client.
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
response,
|
|
16
|
-
serverData,
|
|
17
|
-
}: PageProps) {
|
|
18
|
-
if (!auth.user_id) {
|
|
19
|
-
response.redirect("/login");
|
|
20
|
-
return null;
|
|
21
|
-
}
|
|
11
|
+
// kept live + optimistic by `db` on the client. Auth gate + shell chrome come
|
|
12
|
+
// from the dashboard layout.
|
|
13
|
+
export default function ProjectsPage({ auth, response, serverData }: PageProps) {
|
|
22
14
|
if (!auth.tenant_id) {
|
|
23
15
|
response.redirect("/dashboard");
|
|
24
16
|
return null;
|
|
25
17
|
}
|
|
26
|
-
const me = use(serverData.get<{ email?: string }>("User", auth.user_id));
|
|
27
|
-
const org = use(serverData.get<{ name?: string }>("Org", auth.tenant_id));
|
|
28
18
|
const projects = use(serverData.list<Project>("Project"));
|
|
29
|
-
return
|
|
30
|
-
<DashboardShell
|
|
31
|
-
active="projects"
|
|
32
|
-
title="Projects"
|
|
33
|
-
userEmail={me?.email ?? ""}
|
|
34
|
-
orgName={org?.name}
|
|
35
|
-
>
|
|
36
|
-
<Projects tenantId={auth.tenant_id} initial={projects} />
|
|
37
|
-
</DashboardShell>
|
|
38
|
-
);
|
|
19
|
+
return <Projects tenantId={auth.tenant_id} initial={projects} />;
|
|
39
20
|
}
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import React, { use } from "react";
|
|
2
2
|
import { type Metadata, type PageProps } from "@pylonsync/react";
|
|
3
|
-
import { DashboardShell } from "@/components/dashboard-shell";
|
|
4
3
|
import {
|
|
5
4
|
Settings,
|
|
6
5
|
type OrgInfo,
|
|
@@ -14,34 +13,23 @@ export const metadata: Metadata = {
|
|
|
14
13
|
|
|
15
14
|
// `/dashboard/settings` — workspace settings: rename (owners/admins) and delete
|
|
16
15
|
// (owners). The active org + member count are resolved server-side so the page
|
|
17
|
-
// paints with real values on the first byte.
|
|
16
|
+
// paints with real values on the first byte. Auth gate + shell chrome come from
|
|
17
|
+
// the dashboard layout.
|
|
18
18
|
export default function SettingsPage({ auth, response, serverData }: PageProps) {
|
|
19
|
-
if (!auth.user_id) {
|
|
20
|
-
response.redirect("/login");
|
|
21
|
-
return null;
|
|
22
|
-
}
|
|
23
19
|
if (!auth.tenant_id) {
|
|
24
20
|
response.redirect("/dashboard");
|
|
25
21
|
return null;
|
|
26
22
|
}
|
|
27
|
-
const me = use(serverData.get<{ email?: string }>("User", auth.user_id));
|
|
28
23
|
const org = use(serverData.get<OrgInfo>("Org", auth.tenant_id));
|
|
29
24
|
const members = use(serverData.list<OrgMemberRow>("OrgMember"));
|
|
30
25
|
const memberCount = members.filter(
|
|
31
26
|
(m) => m.orgId === auth.tenant_id,
|
|
32
27
|
).length;
|
|
33
28
|
return (
|
|
34
|
-
<
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
>
|
|
40
|
-
<Settings
|
|
41
|
-
org={org}
|
|
42
|
-
role={auth.roles?.[0] ?? ""}
|
|
43
|
-
memberCount={memberCount}
|
|
44
|
-
/>
|
|
45
|
-
</DashboardShell>
|
|
29
|
+
<Settings
|
|
30
|
+
org={org}
|
|
31
|
+
role={auth.roles?.[0] ?? ""}
|
|
32
|
+
memberCount={memberCount}
|
|
33
|
+
/>
|
|
46
34
|
);
|
|
47
35
|
}
|
|
@@ -6,11 +6,10 @@ interface LayoutProps {
|
|
|
6
6
|
}
|
|
7
7
|
|
|
8
8
|
// The root layout is only the document shell: <html>, <head>, <body>, and the
|
|
9
|
-
// site-wide theme variables.
|
|
10
|
-
// `(marketing)/layout.tsx` adds the nav + footer
|
|
11
|
-
//
|
|
12
|
-
//
|
|
13
|
-
// shell.
|
|
9
|
+
// site-wide theme variables. Each section owns its chrome in its own layout:
|
|
10
|
+
// `(marketing)/layout.tsx` adds the nav + footer, `(auth)/layout.tsx` the
|
|
11
|
+
// split-screen frame for /login + /signup, and `dashboard/layout.tsx` the
|
|
12
|
+
// sidebar shell.
|
|
14
13
|
export default function RootLayout({ children }: LayoutProps) {
|
|
15
14
|
return (
|
|
16
15
|
<html
|
|
@@ -14,9 +14,12 @@ import {
|
|
|
14
14
|
type LucideIcon,
|
|
15
15
|
} from "lucide-react";
|
|
16
16
|
|
|
17
|
-
type NavKey = "overview" | "projects" | "members" | "billing" | "settings";
|
|
17
|
+
export type NavKey = "overview" | "projects" | "members" | "billing" | "settings";
|
|
18
18
|
|
|
19
|
-
|
|
19
|
+
// Exported so `app/dashboard/layout.tsx` can derive the active item + page
|
|
20
|
+
// title from the request URL — one table drives both the sidebar and the
|
|
21
|
+
// layout's routing awareness.
|
|
22
|
+
export const NAV: { key: NavKey; label: string; href: string; Icon: LucideIcon }[] = [
|
|
20
23
|
{ key: "overview", label: "Overview", href: "/dashboard", Icon: LayoutDashboard },
|
|
21
24
|
{ key: "projects", label: "Projects", href: "/dashboard/projects", Icon: FolderKanban },
|
|
22
25
|
{ key: "members", label: "Members", href: "/dashboard/members", Icon: Users },
|
|
@@ -25,10 +28,11 @@ const NAV: { key: NavKey; label: string; href: string; Icon: LucideIcon }[] = [
|
|
|
25
28
|
];
|
|
26
29
|
|
|
27
30
|
// Dashboard chrome: a fixed sidebar (logo, workspace switcher, nav) plus a top
|
|
28
|
-
// bar with a user menu.
|
|
29
|
-
//
|
|
30
|
-
//
|
|
31
|
-
//
|
|
31
|
+
// bar with a user menu. Rendered by `app/dashboard/layout.tsx`, which wraps
|
|
32
|
+
// every /dashboard page — /dashboard sits outside the `(marketing)` route
|
|
33
|
+
// group, so this shell is the only chrome here. `userEmail` is resolved on the
|
|
34
|
+
// server (serverData.get("User", …)) and passed in, so the menu shows a real
|
|
35
|
+
// email instead of a raw id.
|
|
32
36
|
export function DashboardShell({
|
|
33
37
|
active,
|
|
34
38
|
title,
|
|
@@ -1,28 +0,0 @@
|
|
|
1
|
-
import React from "react";
|
|
2
|
-
import { type Metadata, type PageProps } from "@pylonsync/react";
|
|
3
|
-
import { AuthShell } from "../auth-shell";
|
|
4
|
-
import { AuthForm } from "../auth-form";
|
|
5
|
-
|
|
6
|
-
export const metadata: Metadata = {
|
|
7
|
-
title: "Sign in — Acme",
|
|
8
|
-
robots: "noindex",
|
|
9
|
-
};
|
|
10
|
-
|
|
11
|
-
// `app/login/page.tsx` → `/login`. Split-screen auth (form left, brand right).
|
|
12
|
-
// Auth routes sit outside the `(marketing)` route group, so they render bare —
|
|
13
|
-
// no marketing nav or footer.
|
|
14
|
-
export default function LoginPage({ auth, response }: PageProps) {
|
|
15
|
-
// Already signed in? Skip the form. `response.redirect` runs in the
|
|
16
|
-
// synchronous shell render, so it's a real 307 before any HTML is sent.
|
|
17
|
-
if (auth.user_id) response.redirect("/dashboard");
|
|
18
|
-
return (
|
|
19
|
-
<AuthShell
|
|
20
|
-
title="Welcome back"
|
|
21
|
-
switchPrompt="New to Acme?"
|
|
22
|
-
switchLabel="Create an account"
|
|
23
|
-
switchHref="/signup"
|
|
24
|
-
>
|
|
25
|
-
<AuthForm mode="login" />
|
|
26
|
-
</AuthShell>
|
|
27
|
-
);
|
|
28
|
-
}
|
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
import React from "react";
|
|
2
|
-
import { type Metadata, type PageProps } from "@pylonsync/react";
|
|
3
|
-
import { AuthShell } from "../auth-shell";
|
|
4
|
-
import { AuthForm } from "../auth-form";
|
|
5
|
-
|
|
6
|
-
export const metadata: Metadata = {
|
|
7
|
-
title: "Create your account — Acme",
|
|
8
|
-
robots: "noindex",
|
|
9
|
-
};
|
|
10
|
-
|
|
11
|
-
// `app/signup/page.tsx` → `/signup`. Split-screen auth, register mode.
|
|
12
|
-
export default function SignupPage({ auth, response }: PageProps) {
|
|
13
|
-
if (auth.user_id) response.redirect("/dashboard");
|
|
14
|
-
return (
|
|
15
|
-
<AuthShell
|
|
16
|
-
title="Create an account"
|
|
17
|
-
switchPrompt="Already have an account?"
|
|
18
|
-
switchLabel="Sign in"
|
|
19
|
-
switchHref="/login"
|
|
20
|
-
>
|
|
21
|
-
<AuthForm mode="signup" />
|
|
22
|
-
</AuthShell>
|
|
23
|
-
);
|
|
24
|
-
}
|
|
File without changes
|