@pylonsync/create-pylon 0.3.373 → 0.3.374
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/_root/AGENTS.md +1 -1
- package/templates/agency/AGENTS.md +1 -1
- package/templates/agency/app/(marketing)/layout.tsx +136 -0
- package/templates/{creator/app → agency/app/(marketing)}/not-found.tsx +6 -2
- package/templates/agency/app/error.tsx +6 -3
- package/templates/agency/app/layout.tsx +8 -134
- package/templates/agency/app/login/page.tsx +3 -2
- package/templates/ai-chat/AGENTS.md +1 -1
- package/templates/ai-chat/app/(chat)/layout.tsx +46 -0
- package/templates/ai-chat/app/{not-found.tsx → (chat)/not-found.tsx} +5 -2
- package/templates/ai-chat/app/error.tsx +6 -3
- package/templates/ai-chat/app/layout.tsx +6 -39
- package/templates/ai-chat/app/login/page.tsx +2 -1
- package/templates/ai-studio/AGENTS.md +1 -1
- package/templates/ai-studio/app/(studio)/layout.tsx +46 -0
- package/templates/{agency/app → ai-studio/app/(studio)}/not-found.tsx +5 -2
- package/templates/ai-studio/app/error.tsx +6 -3
- package/templates/ai-studio/app/layout.tsx +6 -39
- package/templates/ai-studio/app/login/page.tsx +2 -1
- package/templates/barebones/AGENTS.md +1 -1
- package/templates/chat/AGENTS.md +1 -1
- package/templates/consumer/AGENTS.md +1 -1
- package/templates/creator/AGENTS.md +1 -1
- package/templates/creator/app/(marketing)/layout.tsx +122 -0
- package/templates/{ai-studio/app → creator/app/(marketing)}/not-found.tsx +6 -2
- package/templates/creator/app/error.tsx +6 -3
- package/templates/creator/app/layout.tsx +8 -120
- package/templates/creator/app/login/page.tsx +3 -2
- package/templates/crm/AGENTS.md +1 -1
- package/templates/directory/AGENTS.md +1 -1
- package/templates/directory/app/(marketing)/layout.tsx +133 -0
- package/templates/directory/app/(marketing)/not-found.tsx +23 -0
- package/templates/directory/app/error.tsx +6 -3
- package/templates/directory/app/layout.tsx +8 -131
- package/templates/directory/app/login/page.tsx +3 -2
- package/templates/helpdesk/AGENTS.md +1 -1
- package/templates/inventory/AGENTS.md +1 -1
- package/templates/invoices/AGENTS.md +1 -1
- package/templates/local-service/AGENTS.md +1 -1
- package/templates/local-service/app/(marketing)/layout.tsx +119 -0
- package/templates/local-service/app/(marketing)/not-found.tsx +23 -0
- package/templates/local-service/app/{page.tsx → (marketing)/page.tsx} +2 -2
- package/templates/local-service/app/error.tsx +3 -1
- package/templates/local-service/app/layout.tsx +8 -116
- package/templates/local-service/app/login/page.tsx +3 -2
- package/templates/local-service/components/marketing.tsx +1 -1
- package/templates/marketplace/AGENTS.md +1 -1
- package/templates/projects/AGENTS.md +1 -1
- package/templates/restaurant/AGENTS.md +1 -1
- package/templates/restaurant/app/(marketing)/layout.tsx +119 -0
- package/templates/restaurant/app/(marketing)/not-found.tsx +23 -0
- package/templates/restaurant/app/{page.tsx → (marketing)/page.tsx} +2 -2
- package/templates/restaurant/app/error.tsx +3 -1
- package/templates/restaurant/app/layout.tsx +8 -116
- package/templates/restaurant/app/login/page.tsx +3 -2
- package/templates/restaurant/components/marketing.tsx +1 -1
- package/templates/saas/AGENTS.md +1 -1
- package/templates/saas/app/(marketing)/layout.tsx +411 -0
- package/templates/saas/app/{not-found.tsx → (marketing)/not-found.tsx} +9 -5
- package/templates/saas/app/dashboard/page.tsx +3 -2
- package/templates/saas/app/error.tsx +7 -5
- package/templates/saas/app/layout.tsx +8 -408
- package/templates/saas/app/login/page.tsx +2 -1
- package/templates/saas/components/dashboard-shell.tsx +2 -2
- package/templates/shop/AGENTS.md +1 -1
- package/templates/shop/app/(marketing)/layout.tsx +122 -0
- package/templates/shop/app/(marketing)/not-found.tsx +23 -0
- package/templates/shop/app/{page.tsx → (marketing)/page.tsx} +1 -1
- package/templates/shop/app/error.tsx +3 -1
- package/templates/shop/app/layout.tsx +8 -120
- package/templates/shop/app/login/page.tsx +3 -2
- package/templates/todo/AGENTS.md +1 -1
- package/templates/waitlist/AGENTS.md +1 -1
- package/templates/waitlist/app/(marketing)/layout.tsx +122 -0
- package/templates/waitlist/app/(marketing)/not-found.tsx +23 -0
- package/templates/waitlist/app/{page.tsx → (marketing)/page.tsx} +1 -1
- package/templates/waitlist/app/error.tsx +3 -1
- package/templates/waitlist/app/layout.tsx +8 -120
- package/templates/waitlist/app/login/page.tsx +3 -2
- package/templates/directory/app/not-found.tsx +0 -19
- package/templates/local-service/app/not-found.tsx +0 -19
- package/templates/restaurant/app/not-found.tsx +0 -19
- package/templates/shop/app/not-found.tsx +0 -19
- package/templates/waitlist/app/not-found.tsx +0 -19
- /package/templates/agency/app/{contact-form.tsx → (marketing)/contact-form.tsx} +0 -0
- /package/templates/agency/app/{page.tsx → (marketing)/page.tsx} +0 -0
- /package/templates/agency/app/{seeder.tsx → (marketing)/seeder.tsx} +0 -0
- /package/templates/agency/app/{work → (marketing)/work}/[slug]/page.tsx +0 -0
- /package/templates/agency/app/{work → (marketing)/work}/page.tsx +0 -0
- /package/templates/ai-chat/app/{chat-client.tsx → (chat)/chat-client.tsx} +0 -0
- /package/templates/ai-chat/app/{page.tsx → (chat)/page.tsx} +0 -0
- /package/templates/ai-studio/app/{page.tsx → (studio)/page.tsx} +0 -0
- /package/templates/ai-studio/app/{studio-client.tsx → (studio)/studio-client.tsx} +0 -0
- /package/templates/creator/app/{newsletter-signup.tsx → (marketing)/newsletter-signup.tsx} +0 -0
- /package/templates/creator/app/{page.tsx → (marketing)/page.tsx} +0 -0
- /package/templates/directory/app/{directory-browse.tsx → (marketing)/directory-browse.tsx} +0 -0
- /package/templates/directory/app/{page.tsx → (marketing)/page.tsx} +0 -0
- /package/templates/directory/app/{submit → (marketing)/submit}/page.tsx +0 -0
- /package/templates/directory/app/{submit-form.tsx → (marketing)/submit-form.tsx} +0 -0
- /package/templates/local-service/app/{booking-widget.tsx → (marketing)/booking-widget.tsx} +0 -0
- /package/templates/restaurant/app/{reservation-widget.tsx → (marketing)/reservation-widget.tsx} +0 -0
- /package/templates/saas/app/{company → (marketing)/company}/[slug]/page.tsx +0 -0
- /package/templates/saas/app/{compare → (marketing)/compare}/[slug]/page.tsx +0 -0
- /package/templates/saas/app/{page.tsx → (marketing)/page.tsx} +0 -0
- /package/templates/saas/app/{products → (marketing)/products}/[slug]/page.tsx +0 -0
- /package/templates/saas/app/{resources → (marketing)/resources}/[slug]/page.tsx +0 -0
- /package/templates/saas/app/{solutions → (marketing)/solutions}/[slug]/page.tsx +0 -0
- /package/templates/shop/app/{shop-client.tsx → (marketing)/shop-client.tsx} +0 -0
- /package/templates/shop/app/{success → (marketing)/success}/page.tsx +0 -0
- /package/templates/waitlist/app/{waitlist-hero.tsx → (marketing)/waitlist-hero.tsx} +0 -0
|
@@ -1,32 +1,18 @@
|
|
|
1
1
|
import React from "react";
|
|
2
|
-
import { Link, type PageAuth } from "@pylonsync/react";
|
|
3
2
|
import { siteConfig } from "@/lib/site.config";
|
|
4
3
|
import { SectionScroller } from "@/components/section-scroller";
|
|
5
4
|
|
|
6
|
-
// A layout wraps every page. This marketing layout renders a slim nav up top
|
|
7
|
-
// and a footer below, both driven by lib/site.config.ts. `auth.user_id` is
|
|
8
|
-
// resolved server-side from the session cookie before any HTML is sent, so the
|
|
9
|
-
// nav shows "Dashboard" once the owner is signed in and "Sign in" otherwise —
|
|
10
|
-
// no flash, no client fetch.
|
|
11
5
|
interface LayoutProps {
|
|
12
6
|
children: React.ReactNode;
|
|
13
|
-
url: string;
|
|
14
|
-
auth: PageAuth;
|
|
15
7
|
}
|
|
16
8
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
// The auth screens and the dashboard bring their own chrome, so they render
|
|
25
|
-
// bare (no marketing nav/footer). Match on the path PREFIX, not a substring.
|
|
26
|
-
const path = (url ?? "").split("?")[0];
|
|
27
|
-
const BARE_PREFIXES = ["/login", "/dashboard"];
|
|
28
|
-
const isBare = BARE_PREFIXES.some((p) => path === p || path.startsWith(p + "/"));
|
|
29
|
-
|
|
9
|
+
// The root layout is only the document shell: <html>, <head>, <body>, and the
|
|
10
|
+
// site-wide theme variables. Section chrome lives in route-group layouts —
|
|
11
|
+
// `(marketing)/layout.tsx` adds the nav + footer for every marketing page.
|
|
12
|
+
// Routes outside the group (/login, /dashboard) render bare here: the auth
|
|
13
|
+
// screen and the dashboard bring their own chrome.
|
|
14
|
+
export default function RootLayout({ children }: LayoutProps) {
|
|
15
|
+
const { colors } = siteConfig;
|
|
30
16
|
return (
|
|
31
17
|
<html
|
|
32
18
|
lang="en"
|
|
@@ -53,117 +39,8 @@ export default function RootLayout({ children, url, auth }: LayoutProps) {
|
|
|
53
39
|
</head>
|
|
54
40
|
<body className="flex min-h-screen flex-col bg-background text-foreground antialiased">
|
|
55
41
|
<SectionScroller />
|
|
56
|
-
{
|
|
57
|
-
children
|
|
58
|
-
) : (
|
|
59
|
-
<>
|
|
60
|
-
<header className="sticky top-0 z-30 border-b border-zinc-200/70 bg-white/85 backdrop-blur">
|
|
61
|
-
<div className="mx-auto flex h-14 max-w-5xl items-center justify-between px-6">
|
|
62
|
-
<Link href="/" className="flex items-center gap-2">
|
|
63
|
-
<span className="flex size-6 items-center justify-center rounded-[7px] bg-zinc-900 text-[13px] font-bold text-white">
|
|
64
|
-
{brand.letter}
|
|
65
|
-
</span>
|
|
66
|
-
<span className="text-[15px] font-semibold tracking-tight text-zinc-900">
|
|
67
|
-
{brand.name}
|
|
68
|
-
</span>
|
|
69
|
-
</Link>
|
|
70
|
-
<nav className="flex items-center gap-1 sm:gap-2">
|
|
71
|
-
<a href="/#browse" className="hidden rounded-full px-3 py-1.5 text-[13px] font-medium text-zinc-600 transition-colors hover:text-zinc-900 sm:inline-flex">
|
|
72
|
-
Browse
|
|
73
|
-
</a>
|
|
74
|
-
{signedIn ? (
|
|
75
|
-
<Link
|
|
76
|
-
href="/dashboard"
|
|
77
|
-
className="inline-flex items-center rounded-full bg-zinc-900 px-3.5 py-1.5 text-[13px] font-medium text-white transition-colors hover:bg-zinc-700"
|
|
78
|
-
>
|
|
79
|
-
Dashboard
|
|
80
|
-
</Link>
|
|
81
|
-
) : (
|
|
82
|
-
<>
|
|
83
|
-
<Link
|
|
84
|
-
href="/login"
|
|
85
|
-
className="hidden rounded-full px-3 py-1.5 text-[13px] font-medium text-zinc-600 transition-colors hover:text-zinc-900 sm:inline-flex"
|
|
86
|
-
>
|
|
87
|
-
Sign in
|
|
88
|
-
</Link>
|
|
89
|
-
<Link
|
|
90
|
-
href="/submit"
|
|
91
|
-
className="inline-flex items-center rounded-full bg-brand px-3.5 py-1.5 text-[13px] font-medium text-white transition-opacity hover:opacity-90"
|
|
92
|
-
>
|
|
93
|
-
{siteConfig.hero.ctaLabel}
|
|
94
|
-
</Link>
|
|
95
|
-
</>
|
|
96
|
-
)}
|
|
97
|
-
</nav>
|
|
98
|
-
</div>
|
|
99
|
-
</header>
|
|
100
|
-
|
|
101
|
-
<main className="flex-1">{children}</main>
|
|
102
|
-
|
|
103
|
-
<SiteFooter />
|
|
104
|
-
</>
|
|
105
|
-
)}
|
|
42
|
+
{children}
|
|
106
43
|
</body>
|
|
107
44
|
</html>
|
|
108
45
|
);
|
|
109
46
|
}
|
|
110
|
-
|
|
111
|
-
function SiteFooter() {
|
|
112
|
-
const { brand } = siteConfig;
|
|
113
|
-
return (
|
|
114
|
-
<footer className="border-t border-zinc-200/70 bg-white">
|
|
115
|
-
<div className="mx-auto max-w-5xl px-6 py-12">
|
|
116
|
-
<div className="flex flex-col items-start justify-between gap-6 sm:flex-row">
|
|
117
|
-
<div className="max-w-sm">
|
|
118
|
-
<Link href="/" className="inline-flex items-center gap-2">
|
|
119
|
-
<span className="flex size-6 items-center justify-center rounded-[7px] bg-zinc-900 text-[13px] font-bold text-white">
|
|
120
|
-
{brand.letter}
|
|
121
|
-
</span>
|
|
122
|
-
<span className="text-[15px] font-semibold tracking-tight text-zinc-900">
|
|
123
|
-
{brand.name}
|
|
124
|
-
</span>
|
|
125
|
-
</Link>
|
|
126
|
-
<p className="mt-3 text-[13px] leading-relaxed text-zinc-500">
|
|
127
|
-
{brand.footerBlurb}
|
|
128
|
-
</p>
|
|
129
|
-
</div>
|
|
130
|
-
<div className="flex items-center gap-4">
|
|
131
|
-
{brand.socials.map((s) => (
|
|
132
|
-
<a
|
|
133
|
-
key={s.label}
|
|
134
|
-
href={s.href}
|
|
135
|
-
aria-label={s.label}
|
|
136
|
-
className="text-zinc-400 transition-colors hover:text-zinc-900"
|
|
137
|
-
>
|
|
138
|
-
<svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor">
|
|
139
|
-
<path d={s.path} />
|
|
140
|
-
</svg>
|
|
141
|
-
</a>
|
|
142
|
-
))}
|
|
143
|
-
<a
|
|
144
|
-
href={`mailto:${brand.email}`}
|
|
145
|
-
aria-label="Email"
|
|
146
|
-
className="text-zinc-400 transition-colors hover:text-zinc-900"
|
|
147
|
-
>
|
|
148
|
-
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
|
149
|
-
<rect x="3" y="5" width="18" height="14" rx="2" />
|
|
150
|
-
<path d="m3 7 9 6 9-6" />
|
|
151
|
-
</svg>
|
|
152
|
-
</a>
|
|
153
|
-
</div>
|
|
154
|
-
</div>
|
|
155
|
-
<div className="mt-10 flex flex-col items-start justify-between gap-3 border-t border-zinc-200/70 pt-6 text-[12px] text-zinc-400 sm:flex-row sm:items-center">
|
|
156
|
-
<span>
|
|
157
|
-
© {new Date().getFullYear()} {brand.copyrightName}
|
|
158
|
-
</span>
|
|
159
|
-
<span>
|
|
160
|
-
Built with{" "}
|
|
161
|
-
<a href="https://pylonsync.com" className="font-medium text-zinc-600 hover:text-zinc-900">
|
|
162
|
-
Pylon
|
|
163
|
-
</a>
|
|
164
|
-
</span>
|
|
165
|
-
</div>
|
|
166
|
-
</div>
|
|
167
|
-
</footer>
|
|
168
|
-
);
|
|
169
|
-
}
|
|
@@ -8,8 +8,9 @@ export const metadata: Metadata = {
|
|
|
8
8
|
robots: "noindex",
|
|
9
9
|
};
|
|
10
10
|
|
|
11
|
-
// `app/login/page.tsx` → `/login`. The owner's sign-in.
|
|
12
|
-
//
|
|
11
|
+
// `app/login/page.tsx` → `/login`. The owner's sign-in. It sits outside the
|
|
12
|
+
// `(marketing)` route group, so it renders bare — no marketing nav/footer.
|
|
13
|
+
// Already signed in?
|
|
13
14
|
// Skip straight to the dashboard — `response.redirect` in the synchronous shell
|
|
14
15
|
// render is a real 307 before any HTML is sent.
|
|
15
16
|
export default function LoginPage({ auth, response }: PageProps) {
|
|
@@ -6,7 +6,7 @@ Pylon serves the API, auth, sync, WebSocket, SSE, and native React 19 SSR from o
|
|
|
6
6
|
|
|
7
7
|
**Unified SSR app:**
|
|
8
8
|
- `app.ts`: data model + manifest (`entity()` + `field.*`, queries/actions/policies, `routes: await discoverAppRoutes()`). Ends with `console.log(JSON.stringify(manifest))`.
|
|
9
|
-
- `app/`: file-based SSR routes. `app/page.tsx` → `/`, `app/about/page.tsx` → `/about`, `app/blog/[slug]/page.tsx` → `/blog/:slug`. `app/layout.tsx` is the shell; `app/error.tsx` / `app/not-found.tsx` are boundaries.
|
|
9
|
+
- `app/`: file-based SSR routes. `app/page.tsx` → `/`, `app/about/page.tsx` → `/about`, `app/blog/[slug]/page.tsx` → `/blog/:slug`. A `(group)` directory is stripped from the URL — `app/(marketing)/about/page.tsx` still serves `/about` — so a group's `layout.tsx` gives one section its own chrome (nav, footer) without changing any path; routes outside the group render without it. `app/layout.tsx` is the document shell; `app/error.tsx` / `app/not-found.tsx` are boundaries.
|
|
10
10
|
- `app/globals.css`: Tailwind v4 entrypoint (auto-compiled and injected).
|
|
11
11
|
- `functions/`: server functions, one per file, `default`-exported.
|
|
12
12
|
- `.pylon/`: local dev state (SQLite, jobs, sessions, uploads). Created by `pylon dev`. Do not commit.
|
|
@@ -6,7 +6,7 @@ Pylon serves the API, auth, sync, WebSocket, SSE, and native React 19 SSR from o
|
|
|
6
6
|
|
|
7
7
|
**Unified SSR app:**
|
|
8
8
|
- `app.ts`: data model + manifest (`entity()` + `field.*`, queries/actions/policies, `routes: await discoverAppRoutes()`). Ends with `console.log(JSON.stringify(manifest))`.
|
|
9
|
-
- `app/`: file-based SSR routes. `app/page.tsx` → `/`, `app/about/page.tsx` → `/about`, `app/blog/[slug]/page.tsx` → `/blog/:slug`. `app/layout.tsx` is the shell; `app/error.tsx` / `app/not-found.tsx` are boundaries.
|
|
9
|
+
- `app/`: file-based SSR routes. `app/page.tsx` → `/`, `app/about/page.tsx` → `/about`, `app/blog/[slug]/page.tsx` → `/blog/:slug`. A `(group)` directory is stripped from the URL — `app/(marketing)/about/page.tsx` still serves `/about` — so a group's `layout.tsx` gives one section its own chrome (nav, footer) without changing any path; routes outside the group render without it. `app/layout.tsx` is the document shell; `app/error.tsx` / `app/not-found.tsx` are boundaries.
|
|
10
10
|
- `app/globals.css`: Tailwind v4 entrypoint (auto-compiled and injected).
|
|
11
11
|
- `functions/`: server functions, one per file, `default`-exported.
|
|
12
12
|
- `.pylon/`: local dev state (SQLite, jobs, sessions, uploads). Created by `pylon dev`. Do not commit.
|
|
@@ -6,7 +6,7 @@ Pylon serves the API, auth, sync, WebSocket, SSE, and native React 19 SSR from o
|
|
|
6
6
|
|
|
7
7
|
**Unified SSR app:**
|
|
8
8
|
- `app.ts`: data model + manifest (`entity()` + `field.*`, queries/actions/policies, `routes: await discoverAppRoutes()`). Ends with `console.log(JSON.stringify(manifest))`.
|
|
9
|
-
- `app/`: file-based SSR routes. `app/page.tsx` → `/`, `app/about/page.tsx` → `/about`, `app/blog/[slug]/page.tsx` → `/blog/:slug`. `app/layout.tsx` is the shell; `app/error.tsx` / `app/not-found.tsx` are boundaries.
|
|
9
|
+
- `app/`: file-based SSR routes. `app/page.tsx` → `/`, `app/about/page.tsx` → `/about`, `app/blog/[slug]/page.tsx` → `/blog/:slug`. A `(group)` directory is stripped from the URL — `app/(marketing)/about/page.tsx` still serves `/about` — so a group's `layout.tsx` gives one section its own chrome (nav, footer) without changing any path; routes outside the group render without it. `app/layout.tsx` is the document shell; `app/error.tsx` / `app/not-found.tsx` are boundaries.
|
|
10
10
|
- `app/globals.css`: Tailwind v4 entrypoint (auto-compiled and injected).
|
|
11
11
|
- `functions/`: server functions, one per file, `default`-exported.
|
|
12
12
|
- `.pylon/`: local dev state (SQLite, jobs, sessions, uploads). Created by `pylon dev`. Do not commit.
|
|
@@ -6,7 +6,7 @@ Pylon serves the API, auth, sync, WebSocket, SSE, and native React 19 SSR from o
|
|
|
6
6
|
|
|
7
7
|
**Unified SSR app:**
|
|
8
8
|
- `app.ts`: data model + manifest (`entity()` + `field.*`, queries/actions/policies, `routes: await discoverAppRoutes()`). Ends with `console.log(JSON.stringify(manifest))`.
|
|
9
|
-
- `app/`: file-based SSR routes. `app/page.tsx` → `/`, `app/about/page.tsx` → `/about`, `app/blog/[slug]/page.tsx` → `/blog/:slug`. `app/layout.tsx` is the shell; `app/error.tsx` / `app/not-found.tsx` are boundaries.
|
|
9
|
+
- `app/`: file-based SSR routes. `app/page.tsx` → `/`, `app/about/page.tsx` → `/about`, `app/blog/[slug]/page.tsx` → `/blog/:slug`. A `(group)` directory is stripped from the URL — `app/(marketing)/about/page.tsx` still serves `/about` — so a group's `layout.tsx` gives one section its own chrome (nav, footer) without changing any path; routes outside the group render without it. `app/layout.tsx` is the document shell; `app/error.tsx` / `app/not-found.tsx` are boundaries.
|
|
10
10
|
- `app/globals.css`: Tailwind v4 entrypoint (auto-compiled and injected).
|
|
11
11
|
- `functions/`: server functions, one per file, `default`-exported.
|
|
12
12
|
- `.pylon/`: local dev state (SQLite, jobs, sessions, uploads). Created by `pylon dev`. Do not commit.
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { Link, type PageAuth } from "@pylonsync/react";
|
|
3
|
+
import { siteConfig } from "@/lib/site.config";
|
|
4
|
+
|
|
5
|
+
// `(marketing)` is a ROUTE GROUP: the parens segment is stripped from every
|
|
6
|
+
// URL (so `(marketing)/page.tsx` still serves `/`), and this layout wraps only
|
|
7
|
+
// the pages inside the group — a slim nav and a footer, both driven by
|
|
8
|
+
// lib/site.config.ts. /login and /dashboard sit outside the group and render
|
|
9
|
+
// bare in the root shell. `auth.user_id` is resolved server-side from the
|
|
10
|
+
// session cookie before any HTML is sent, so the owner sees "Dashboard" and
|
|
11
|
+
// everyone else sees "Sign in" — no flash, no client fetch.
|
|
12
|
+
interface LayoutProps {
|
|
13
|
+
children: React.ReactNode;
|
|
14
|
+
auth: PageAuth;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export default function MarketingLayout({ children, auth }: LayoutProps) {
|
|
18
|
+
// A guest session (minted by <EnsureGuest> for the live picker) has a
|
|
19
|
+
// `guest_…` user id — that's an anonymous visitor, NOT the signed-in owner,
|
|
20
|
+
// so it shouldn't flip the nav to "Dashboard".
|
|
21
|
+
const signedIn = Boolean(auth?.user_id && !auth.user_id.startsWith("guest_"));
|
|
22
|
+
const { brand } = siteConfig;
|
|
23
|
+
|
|
24
|
+
return (
|
|
25
|
+
<>
|
|
26
|
+
<header className="sticky top-0 z-30 border-b border-zinc-200/70 bg-white/85 backdrop-blur">
|
|
27
|
+
<div className="mx-auto flex h-14 max-w-5xl items-center justify-between px-6">
|
|
28
|
+
<Link href="/" className="flex items-center gap-2">
|
|
29
|
+
<span className="flex size-6 items-center justify-center rounded-[7px] bg-zinc-900 text-[13px] font-bold text-white">
|
|
30
|
+
{brand.letter}
|
|
31
|
+
</span>
|
|
32
|
+
<span className="text-[15px] font-semibold tracking-tight text-zinc-900">
|
|
33
|
+
{brand.name}
|
|
34
|
+
</span>
|
|
35
|
+
</Link>
|
|
36
|
+
<nav className="flex items-center gap-2">
|
|
37
|
+
<a
|
|
38
|
+
href="/#services"
|
|
39
|
+
className="hidden rounded-full px-3 py-1.5 text-[13px] font-medium text-zinc-600 transition-colors hover:text-zinc-900 sm:inline-flex"
|
|
40
|
+
>
|
|
41
|
+
Services
|
|
42
|
+
</a>
|
|
43
|
+
{signedIn ? (
|
|
44
|
+
<Link
|
|
45
|
+
href="/dashboard"
|
|
46
|
+
className="inline-flex items-center rounded-full bg-zinc-900 px-3.5 py-1.5 text-[13px] font-medium text-white transition-colors hover:bg-zinc-700"
|
|
47
|
+
>
|
|
48
|
+
Dashboard
|
|
49
|
+
</Link>
|
|
50
|
+
) : (
|
|
51
|
+
<>
|
|
52
|
+
<Link
|
|
53
|
+
href="/login"
|
|
54
|
+
className="hidden rounded-full px-3 py-1.5 text-[13px] font-medium text-zinc-600 transition-colors hover:text-zinc-900 sm:inline-flex"
|
|
55
|
+
>
|
|
56
|
+
Sign in
|
|
57
|
+
</Link>
|
|
58
|
+
<a
|
|
59
|
+
href="/#book"
|
|
60
|
+
className="inline-flex items-center rounded-full bg-brand px-3.5 py-1.5 text-[13px] font-medium text-white transition-colors hover:opacity-90"
|
|
61
|
+
>
|
|
62
|
+
{siteConfig.hero.ctaLabel}
|
|
63
|
+
</a>
|
|
64
|
+
</>
|
|
65
|
+
)}
|
|
66
|
+
</nav>
|
|
67
|
+
</div>
|
|
68
|
+
</header>
|
|
69
|
+
|
|
70
|
+
<main className="flex-1">{children}</main>
|
|
71
|
+
|
|
72
|
+
<SiteFooter />
|
|
73
|
+
</>
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function SiteFooter() {
|
|
78
|
+
const { brand, location } = siteConfig;
|
|
79
|
+
return (
|
|
80
|
+
<footer className="border-t border-zinc-200/70 bg-white">
|
|
81
|
+
<div className="mx-auto max-w-5xl px-6 py-12">
|
|
82
|
+
<div className="flex flex-col items-start justify-between gap-6 sm:flex-row">
|
|
83
|
+
<div className="max-w-sm">
|
|
84
|
+
<Link href="/" className="inline-flex items-center gap-2">
|
|
85
|
+
<span className="flex size-6 items-center justify-center rounded-[7px] bg-zinc-900 text-[13px] font-bold text-white">
|
|
86
|
+
{brand.letter}
|
|
87
|
+
</span>
|
|
88
|
+
<span className="text-[15px] font-semibold tracking-tight text-zinc-900">
|
|
89
|
+
{brand.name}
|
|
90
|
+
</span>
|
|
91
|
+
</Link>
|
|
92
|
+
<p className="mt-3 text-[13px] leading-relaxed text-zinc-500">
|
|
93
|
+
{brand.footerBlurb}
|
|
94
|
+
</p>
|
|
95
|
+
</div>
|
|
96
|
+
<div className="text-[13px] leading-relaxed text-zinc-500">
|
|
97
|
+
<div className="font-medium text-zinc-900">Visit</div>
|
|
98
|
+
<p className="mt-2 max-w-[14rem]">{location.address}</p>
|
|
99
|
+
<p className="mt-2">{location.phone}</p>
|
|
100
|
+
<a href={`mailto:${brand.email}`} className="mt-1 inline-block hover:text-zinc-900">
|
|
101
|
+
{brand.email}
|
|
102
|
+
</a>
|
|
103
|
+
</div>
|
|
104
|
+
</div>
|
|
105
|
+
<div className="mt-10 flex flex-col items-start justify-between gap-3 border-t border-zinc-200/70 pt-6 text-[12px] text-zinc-400 sm:flex-row sm:items-center">
|
|
106
|
+
<span>
|
|
107
|
+
© {new Date().getFullYear()} {brand.copyrightName}
|
|
108
|
+
</span>
|
|
109
|
+
<span>
|
|
110
|
+
Built with{" "}
|
|
111
|
+
<a href="https://pylonsync.com" className="font-medium text-zinc-600 hover:text-zinc-900">
|
|
112
|
+
Pylon
|
|
113
|
+
</a>
|
|
114
|
+
</span>
|
|
115
|
+
</div>
|
|
116
|
+
</div>
|
|
117
|
+
</footer>
|
|
118
|
+
);
|
|
119
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { Link, type NotFoundProps } from "@pylonsync/react";
|
|
3
|
+
|
|
4
|
+
// `(marketing)/not-found.tsx` → rendered at HTTP 404 for any unmatched URL
|
|
5
|
+
// (and when a page calls `response.notFound()`). It lives inside the
|
|
6
|
+
// `(marketing)` group on purpose: a group segment adds no URL prefix, so this
|
|
7
|
+
// is still the root 404 boundary, but it wraps in the marketing layout — a
|
|
8
|
+
// missing URL gets the site nav + footer instead of a bare page. Hydrated, so
|
|
9
|
+
// the link is a client nav.
|
|
10
|
+
export default function NotFound(_props: NotFoundProps) {
|
|
11
|
+
return (
|
|
12
|
+
<div className="mx-auto flex min-h-[60vh] max-w-3xl flex-col items-center justify-center px-6 text-center">
|
|
13
|
+
<h1 className="text-3xl font-semibold tracking-tight">404</h1>
|
|
14
|
+
<p className="mt-2 text-zinc-500">We couldn't find that page.</p>
|
|
15
|
+
<Link
|
|
16
|
+
href="/"
|
|
17
|
+
className="mt-6 inline-flex h-10 items-center rounded-full bg-zinc-900 px-5 text-sm font-medium text-white transition-colors hover:bg-zinc-700"
|
|
18
|
+
>
|
|
19
|
+
Back home
|
|
20
|
+
</Link>
|
|
21
|
+
</div>
|
|
22
|
+
);
|
|
23
|
+
}
|
|
@@ -21,7 +21,7 @@ export const metadata: Metadata = {
|
|
|
21
21
|
},
|
|
22
22
|
};
|
|
23
23
|
|
|
24
|
-
// `app/page.tsx` →
|
|
24
|
+
// `app/(marketing)/page.tsx` → `/` (the group segment adds no URL prefix). Server-rendered single-page site for an appointment
|
|
25
25
|
// business. Hero, services, reviews, location, and FAQ are static server HTML
|
|
26
26
|
// (SEO + first paint); the booking section (#book) is a client island
|
|
27
27
|
// (<BookingWidget>) with live slot availability. All copy comes from siteConfig.
|
|
@@ -69,7 +69,7 @@ export default function LandingPage() {
|
|
|
69
69
|
<ImagePlaceholder
|
|
70
70
|
shape="portrait"
|
|
71
71
|
title="A photo of your shop"
|
|
72
|
-
hint="Swap for an <img> in app/page.tsx"
|
|
72
|
+
hint="Swap for an <img> in app/(marketing)/page.tsx"
|
|
73
73
|
/>
|
|
74
74
|
<div className="absolute left-4 top-4">
|
|
75
75
|
<LiveBadge>Availability updates live</LiveBadge>
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import React from "react";
|
|
2
2
|
import { type ErrorBoundaryProps } from "@pylonsync/react";
|
|
3
3
|
|
|
4
|
-
// `app/error.tsx` → the error boundary
|
|
4
|
+
// `app/error.tsx` → the GLOBAL error boundary. Sitting at the app root
|
|
5
|
+
// (outside the `(marketing)` group), it catches a throw from any section and
|
|
6
|
+
// renders at HTTP 500 in the bare root shell. Hydrated + interactive:
|
|
5
7
|
// `reset()` re-attempts the route. The thrown error reaches the client as
|
|
6
8
|
// `{ message, digest }` only — the stack stays in the dev overlay / server logs.
|
|
7
9
|
export default function Error({ error, reset }: ErrorBoundaryProps) {
|
|
@@ -1,30 +1,18 @@
|
|
|
1
1
|
import React from "react";
|
|
2
|
-
import { Link, type PageAuth } from "@pylonsync/react";
|
|
3
2
|
import { siteConfig } from "@/lib/site.config";
|
|
4
3
|
import { SectionScroller } from "@/components/section-scroller";
|
|
5
4
|
|
|
6
|
-
// A layout wraps every page. This marketing layout renders a slim nav and a
|
|
7
|
-
// footer, both driven by lib/site.config.ts. `auth.user_id` is resolved
|
|
8
|
-
// server-side from the session cookie before any HTML is sent, so the owner
|
|
9
|
-
// sees "Dashboard" and everyone else sees "Sign in" — no flash, no client fetch.
|
|
10
5
|
interface LayoutProps {
|
|
11
6
|
children: React.ReactNode;
|
|
12
|
-
url: string;
|
|
13
|
-
auth: PageAuth;
|
|
14
7
|
}
|
|
15
8
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
// Auth + dashboard render bare (no marketing chrome). Match the path PREFIX.
|
|
24
|
-
const path = (url ?? "").split("?")[0];
|
|
25
|
-
const BARE_PREFIXES = ["/login", "/dashboard"];
|
|
26
|
-
const isBare = BARE_PREFIXES.some((p) => path === p || path.startsWith(p + "/"));
|
|
27
|
-
|
|
9
|
+
// The root layout is only the document shell: <html>, <head>, <body>, and the
|
|
10
|
+
// site-wide theme variables. Section chrome lives in route-group layouts —
|
|
11
|
+
// `(marketing)/layout.tsx` adds the nav + footer for the marketing pages.
|
|
12
|
+
// /login and /dashboard sit outside the group and render bare here: the auth
|
|
13
|
+
// screen and the dashboard bring their own chrome.
|
|
14
|
+
export default function RootLayout({ children }: LayoutProps) {
|
|
15
|
+
const { colors } = siteConfig;
|
|
28
16
|
return (
|
|
29
17
|
<html
|
|
30
18
|
lang="en"
|
|
@@ -46,104 +34,8 @@ export default function RootLayout({ children, url, auth }: LayoutProps) {
|
|
|
46
34
|
</head>
|
|
47
35
|
<body className="flex min-h-screen flex-col bg-background text-foreground antialiased">
|
|
48
36
|
<SectionScroller />
|
|
49
|
-
{
|
|
50
|
-
children
|
|
51
|
-
) : (
|
|
52
|
-
<>
|
|
53
|
-
<header className="sticky top-0 z-30 border-b border-zinc-200/70 bg-white/85 backdrop-blur">
|
|
54
|
-
<div className="mx-auto flex h-14 max-w-5xl items-center justify-between px-6">
|
|
55
|
-
<Link href="/" className="flex items-center gap-2">
|
|
56
|
-
<span className="flex size-6 items-center justify-center rounded-[7px] bg-zinc-900 text-[13px] font-bold text-white">
|
|
57
|
-
{brand.letter}
|
|
58
|
-
</span>
|
|
59
|
-
<span className="text-[15px] font-semibold tracking-tight text-zinc-900">
|
|
60
|
-
{brand.name}
|
|
61
|
-
</span>
|
|
62
|
-
</Link>
|
|
63
|
-
<nav className="flex items-center gap-2">
|
|
64
|
-
<a
|
|
65
|
-
href="/#services"
|
|
66
|
-
className="hidden rounded-full px-3 py-1.5 text-[13px] font-medium text-zinc-600 transition-colors hover:text-zinc-900 sm:inline-flex"
|
|
67
|
-
>
|
|
68
|
-
Services
|
|
69
|
-
</a>
|
|
70
|
-
{signedIn ? (
|
|
71
|
-
<Link
|
|
72
|
-
href="/dashboard"
|
|
73
|
-
className="inline-flex items-center rounded-full bg-zinc-900 px-3.5 py-1.5 text-[13px] font-medium text-white transition-colors hover:bg-zinc-700"
|
|
74
|
-
>
|
|
75
|
-
Dashboard
|
|
76
|
-
</Link>
|
|
77
|
-
) : (
|
|
78
|
-
<>
|
|
79
|
-
<Link
|
|
80
|
-
href="/login"
|
|
81
|
-
className="hidden rounded-full px-3 py-1.5 text-[13px] font-medium text-zinc-600 transition-colors hover:text-zinc-900 sm:inline-flex"
|
|
82
|
-
>
|
|
83
|
-
Sign in
|
|
84
|
-
</Link>
|
|
85
|
-
<a
|
|
86
|
-
href="/#book"
|
|
87
|
-
className="inline-flex items-center rounded-full bg-brand px-3.5 py-1.5 text-[13px] font-medium text-white transition-colors hover:opacity-90"
|
|
88
|
-
>
|
|
89
|
-
{siteConfig.hero.ctaLabel}
|
|
90
|
-
</a>
|
|
91
|
-
</>
|
|
92
|
-
)}
|
|
93
|
-
</nav>
|
|
94
|
-
</div>
|
|
95
|
-
</header>
|
|
96
|
-
|
|
97
|
-
<main className="flex-1">{children}</main>
|
|
98
|
-
|
|
99
|
-
<SiteFooter />
|
|
100
|
-
</>
|
|
101
|
-
)}
|
|
37
|
+
{children}
|
|
102
38
|
</body>
|
|
103
39
|
</html>
|
|
104
40
|
);
|
|
105
41
|
}
|
|
106
|
-
|
|
107
|
-
function SiteFooter() {
|
|
108
|
-
const { brand, location } = siteConfig;
|
|
109
|
-
return (
|
|
110
|
-
<footer className="border-t border-zinc-200/70 bg-white">
|
|
111
|
-
<div className="mx-auto max-w-5xl px-6 py-12">
|
|
112
|
-
<div className="flex flex-col items-start justify-between gap-6 sm:flex-row">
|
|
113
|
-
<div className="max-w-sm">
|
|
114
|
-
<Link href="/" className="inline-flex items-center gap-2">
|
|
115
|
-
<span className="flex size-6 items-center justify-center rounded-[7px] bg-zinc-900 text-[13px] font-bold text-white">
|
|
116
|
-
{brand.letter}
|
|
117
|
-
</span>
|
|
118
|
-
<span className="text-[15px] font-semibold tracking-tight text-zinc-900">
|
|
119
|
-
{brand.name}
|
|
120
|
-
</span>
|
|
121
|
-
</Link>
|
|
122
|
-
<p className="mt-3 text-[13px] leading-relaxed text-zinc-500">
|
|
123
|
-
{brand.footerBlurb}
|
|
124
|
-
</p>
|
|
125
|
-
</div>
|
|
126
|
-
<div className="text-[13px] leading-relaxed text-zinc-500">
|
|
127
|
-
<div className="font-medium text-zinc-900">Visit</div>
|
|
128
|
-
<p className="mt-2 max-w-[14rem]">{location.address}</p>
|
|
129
|
-
<p className="mt-2">{location.phone}</p>
|
|
130
|
-
<a href={`mailto:${brand.email}`} className="mt-1 inline-block hover:text-zinc-900">
|
|
131
|
-
{brand.email}
|
|
132
|
-
</a>
|
|
133
|
-
</div>
|
|
134
|
-
</div>
|
|
135
|
-
<div className="mt-10 flex flex-col items-start justify-between gap-3 border-t border-zinc-200/70 pt-6 text-[12px] text-zinc-400 sm:flex-row sm:items-center">
|
|
136
|
-
<span>
|
|
137
|
-
© {new Date().getFullYear()} {brand.copyrightName}
|
|
138
|
-
</span>
|
|
139
|
-
<span>
|
|
140
|
-
Built with{" "}
|
|
141
|
-
<a href="https://pylonsync.com" className="font-medium text-zinc-600 hover:text-zinc-900">
|
|
142
|
-
Pylon
|
|
143
|
-
</a>
|
|
144
|
-
</span>
|
|
145
|
-
</div>
|
|
146
|
-
</div>
|
|
147
|
-
</footer>
|
|
148
|
-
);
|
|
149
|
-
}
|
|
@@ -8,8 +8,9 @@ export const metadata: Metadata = {
|
|
|
8
8
|
robots: "noindex",
|
|
9
9
|
};
|
|
10
10
|
|
|
11
|
-
// `app/login/page.tsx` → `/login`. The owner's sign-in. Rendered bare
|
|
12
|
-
//
|
|
11
|
+
// `app/login/page.tsx` → `/login`. The owner's sign-in. Rendered bare —
|
|
12
|
+
// /login sits outside the `(marketing)` route group, so no marketing nav or
|
|
13
|
+
// footer. Already signed in?
|
|
13
14
|
// Skip straight to the dashboard — `response.redirect` in the synchronous shell
|
|
14
15
|
// render is a real 307 before any HTML is sent.
|
|
15
16
|
export default function LoginPage({ auth, response }: PageProps) {
|
|
@@ -100,7 +100,7 @@ export function initials(name: string) {
|
|
|
100
100
|
//
|
|
101
101
|
// shape — "landscape" | "portrait" | "square" | "circle"
|
|
102
102
|
// title — what photo belongs here ("A photo of your shop")
|
|
103
|
-
// hint — how to replace it ("Swap for an <img> in app/page.tsx")
|
|
103
|
+
// hint — how to replace it ("Swap for an <img> in app/(marketing)/page.tsx")
|
|
104
104
|
export function ImagePlaceholder({
|
|
105
105
|
shape = "landscape",
|
|
106
106
|
title,
|
|
@@ -6,7 +6,7 @@ Pylon serves the API, auth, sync, WebSocket, SSE, and native React 19 SSR from o
|
|
|
6
6
|
|
|
7
7
|
**Unified SSR app:**
|
|
8
8
|
- `app.ts`: data model + manifest (`entity()` + `field.*`, queries/actions/policies, `routes: await discoverAppRoutes()`). Ends with `console.log(JSON.stringify(manifest))`.
|
|
9
|
-
- `app/`: file-based SSR routes. `app/page.tsx` → `/`, `app/about/page.tsx` → `/about`, `app/blog/[slug]/page.tsx` → `/blog/:slug`. `app/layout.tsx` is the shell; `app/error.tsx` / `app/not-found.tsx` are boundaries.
|
|
9
|
+
- `app/`: file-based SSR routes. `app/page.tsx` → `/`, `app/about/page.tsx` → `/about`, `app/blog/[slug]/page.tsx` → `/blog/:slug`. A `(group)` directory is stripped from the URL — `app/(marketing)/about/page.tsx` still serves `/about` — so a group's `layout.tsx` gives one section its own chrome (nav, footer) without changing any path; routes outside the group render without it. `app/layout.tsx` is the document shell; `app/error.tsx` / `app/not-found.tsx` are boundaries.
|
|
10
10
|
- `app/globals.css`: Tailwind v4 entrypoint (auto-compiled and injected).
|
|
11
11
|
- `functions/`: server functions, one per file, `default`-exported.
|
|
12
12
|
- `.pylon/`: local dev state (SQLite, jobs, sessions, uploads). Created by `pylon dev`. Do not commit.
|
|
@@ -6,7 +6,7 @@ Pylon serves the API, auth, sync, WebSocket, SSE, and native React 19 SSR from o
|
|
|
6
6
|
|
|
7
7
|
**Unified SSR app:**
|
|
8
8
|
- `app.ts`: data model + manifest (`entity()` + `field.*`, queries/actions/policies, `routes: await discoverAppRoutes()`). Ends with `console.log(JSON.stringify(manifest))`.
|
|
9
|
-
- `app/`: file-based SSR routes. `app/page.tsx` → `/`, `app/about/page.tsx` → `/about`, `app/blog/[slug]/page.tsx` → `/blog/:slug`. `app/layout.tsx` is the shell; `app/error.tsx` / `app/not-found.tsx` are boundaries.
|
|
9
|
+
- `app/`: file-based SSR routes. `app/page.tsx` → `/`, `app/about/page.tsx` → `/about`, `app/blog/[slug]/page.tsx` → `/blog/:slug`. A `(group)` directory is stripped from the URL — `app/(marketing)/about/page.tsx` still serves `/about` — so a group's `layout.tsx` gives one section its own chrome (nav, footer) without changing any path; routes outside the group render without it. `app/layout.tsx` is the document shell; `app/error.tsx` / `app/not-found.tsx` are boundaries.
|
|
10
10
|
- `app/globals.css`: Tailwind v4 entrypoint (auto-compiled and injected).
|
|
11
11
|
- `functions/`: server functions, one per file, `default`-exported.
|
|
12
12
|
- `.pylon/`: local dev state (SQLite, jobs, sessions, uploads). Created by `pylon dev`. Do not commit.
|
|
@@ -6,7 +6,7 @@ Pylon serves the API, auth, sync, WebSocket, SSE, and native React 19 SSR from o
|
|
|
6
6
|
|
|
7
7
|
**Unified SSR app:**
|
|
8
8
|
- `app.ts`: data model + manifest (`entity()` + `field.*`, queries/actions/policies, `routes: await discoverAppRoutes()`). Ends with `console.log(JSON.stringify(manifest))`.
|
|
9
|
-
- `app/`: file-based SSR routes. `app/page.tsx` → `/`, `app/about/page.tsx` → `/about`, `app/blog/[slug]/page.tsx` → `/blog/:slug`. `app/layout.tsx` is the shell; `app/error.tsx` / `app/not-found.tsx` are boundaries.
|
|
9
|
+
- `app/`: file-based SSR routes. `app/page.tsx` → `/`, `app/about/page.tsx` → `/about`, `app/blog/[slug]/page.tsx` → `/blog/:slug`. A `(group)` directory is stripped from the URL — `app/(marketing)/about/page.tsx` still serves `/about` — so a group's `layout.tsx` gives one section its own chrome (nav, footer) without changing any path; routes outside the group render without it. `app/layout.tsx` is the document shell; `app/error.tsx` / `app/not-found.tsx` are boundaries.
|
|
10
10
|
- `app/globals.css`: Tailwind v4 entrypoint (auto-compiled and injected).
|
|
11
11
|
- `functions/`: server functions, one per file, `default`-exported.
|
|
12
12
|
- `.pylon/`: local dev state (SQLite, jobs, sessions, uploads). Created by `pylon dev`. Do not commit.
|