@pylonsync/create-pylon 0.3.295 → 0.3.297
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/agency/app/globals.css +8 -1
- package/templates/agency/app/layout.tsx +4 -6
- package/templates/agency/app.ts +15 -0
- package/templates/agency/components/marketing.tsx +2 -4
- package/templates/agency/lib/site.config.ts +3 -5
- package/templates/ai-chat/app/globals.css +8 -1
- package/templates/ai-chat/app/layout.tsx +4 -8
- package/templates/ai-chat/app.ts +18 -5
- package/templates/ai-chat/lib/site.config.ts +3 -3
- package/templates/ai-studio/app/globals.css +8 -1
- package/templates/ai-studio/app/layout.tsx +7 -11
- package/templates/ai-studio/app.ts +20 -7
- package/templates/ai-studio/lib/site.config.ts +2 -2
- package/templates/ai-studio/lib/studio.ts +1 -1
- package/templates/backend/b2b/apps/api/schema.ts +10 -24
- package/templates/backend/consumer/apps/api/schema.ts +4 -7
- package/templates/barebones/app/layout.tsx +3 -3
- package/templates/barebones/app.ts +2 -3
- package/templates/chat/app.ts +3 -9
- package/templates/consumer/app.ts +2 -3
- package/templates/creator/.env.example +3 -3
- package/templates/creator/app/globals.css +8 -1
- package/templates/creator/app/layout.tsx +4 -6
- package/templates/creator/app.ts +23 -10
- package/templates/creator/components/marketing.tsx +2 -4
- package/templates/default/app/globals.css +8 -1
- package/templates/default/app/layout.tsx +6 -14
- package/templates/default/app.ts +15 -0
- package/templates/default/lib/products.ts +2 -3
- package/templates/default/lib/site.config.ts +1 -2
- package/templates/default/lib/site.ts +3 -4
- package/templates/directory/app/auth-form.tsx +5 -5
- package/templates/directory/app/globals.css +8 -1
- package/templates/directory/app/layout.tsx +4 -6
- package/templates/directory/app/sitemap.ts +1 -1
- package/templates/directory/app.ts +15 -0
- package/templates/directory/lib/owner.ts +5 -5
- package/templates/expo/chat/apps/expo/App.tsx +2 -3
- package/templates/local-service/app/auth-form.tsx +4 -4
- package/templates/local-service/app/globals.css +8 -1
- package/templates/local-service/app/layout.tsx +4 -6
- package/templates/local-service/app/sitemap.ts +1 -1
- package/templates/local-service/app.ts +15 -0
- package/templates/local-service/components/marketing.tsx +2 -4
- package/templates/local-service/lib/owner.ts +3 -3
- package/templates/local-service/lib/site.config.ts +4 -6
- package/templates/marketplace/app/listing/[id]/page.tsx +3 -3
- package/templates/marketplace/functions/makeOffer.ts +0 -1
- package/templates/restaurant/app/auth-form.tsx +5 -5
- package/templates/restaurant/app/globals.css +8 -1
- package/templates/restaurant/app/layout.tsx +4 -6
- package/templates/restaurant/app/sitemap.ts +1 -1
- package/templates/restaurant/app.ts +15 -0
- package/templates/restaurant/lib/owner.ts +5 -5
- package/templates/shop/app/auth-form.tsx +5 -5
- package/templates/shop/app/globals.css +8 -1
- package/templates/shop/app/layout.tsx +5 -7
- package/templates/shop/app/sitemap.ts +1 -1
- package/templates/shop/app.ts +15 -0
- package/templates/shop/lib/owner.ts +6 -5
- package/templates/todo/app/layout.tsx +2 -2
- package/templates/todo/app/sitemap.ts +1 -1
- package/templates/todo/app.ts +4 -5
- package/templates/vite/todo/apps/web/vite.config.ts +5 -9
- package/templates/waitlist/app/globals.css +8 -1
- package/templates/waitlist/app/layout.tsx +4 -6
- package/templates/waitlist/app/waitlist-hero.tsx +4 -5
- package/templates/waitlist/app.ts +26 -9
- package/templates/waitlist/lib/site.config.ts +3 -5
- package/templates/web/barebones/apps/web/postcss.config.mjs +0 -1
- package/templates/web/barebones/apps/web/src/app/globals.css +1 -4
|
@@ -144,7 +144,14 @@
|
|
|
144
144
|
body {
|
|
145
145
|
background-color: var(--color-background);
|
|
146
146
|
color: var(--color-foreground);
|
|
147
|
-
font-family:
|
|
147
|
+
font-family: var(
|
|
148
|
+
--font-sans,
|
|
149
|
+
Inter,
|
|
150
|
+
ui-sans-serif,
|
|
151
|
+
system-ui,
|
|
152
|
+
-apple-system,
|
|
153
|
+
sans-serif
|
|
154
|
+
);
|
|
148
155
|
-webkit-font-smoothing: antialiased;
|
|
149
156
|
}
|
|
150
157
|
button {
|
|
@@ -205,20 +205,12 @@ export default function RootLayout({ children, url, auth }: LayoutProps) {
|
|
|
205
205
|
`generateMetadata` sets it. A hardcoded title in the layout would
|
|
206
206
|
render first and win over the page's, so every tab would read
|
|
207
207
|
"Acme". */}
|
|
208
|
-
{/* Inter
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
crossOrigin="anonymous"
|
|
215
|
-
/>
|
|
216
|
-
<link
|
|
217
|
-
rel="stylesheet"
|
|
218
|
-
href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap"
|
|
219
|
-
/>
|
|
220
|
-
{/* Tailwind is compiled by Pylon from app/globals.css and the
|
|
221
|
-
stylesheet link is injected here automatically. */}
|
|
208
|
+
{/* Inter is declared in app.ts (`fonts: [font({ family: "Inter", … })]`)
|
|
209
|
+
and self-hosted by the build — the runtime injects the @font-face,
|
|
210
|
+
<link rel=preload>, and a size-adjusted fallback here automatically.
|
|
211
|
+
No third-party Google Fonts request, no layout shift. Swap the family
|
|
212
|
+
in app.ts to change it. Tailwind's compiled stylesheet is injected
|
|
213
|
+
here too. */}
|
|
222
214
|
</head>
|
|
223
215
|
<body className="flex min-h-screen flex-col bg-background text-foreground antialiased">
|
|
224
216
|
{isBare ? (
|
package/templates/default/app.ts
CHANGED
|
@@ -5,6 +5,7 @@ import {
|
|
|
5
5
|
auth,
|
|
6
6
|
buildManifest,
|
|
7
7
|
discoverAppRoutes,
|
|
8
|
+
font,
|
|
8
9
|
} from "@pylonsync/sdk";
|
|
9
10
|
// Per-workspace Stripe billing — see lib/billing.ts. `billing.manifest` brings
|
|
10
11
|
// the StripeSubscription entity + checkout/portal/cancel/restore/webhook actions
|
|
@@ -186,6 +187,20 @@ const manifest = buildManifest({
|
|
|
186
187
|
// above are named with the framework defaults (Org / OrgMember / OrgInvite),
|
|
187
188
|
// so `/api/auth/orgs/*` + `/api/auth/select-org` work with no extra config.
|
|
188
189
|
auth: auth(),
|
|
190
|
+
// Self-hosted Inter (next/font parity): the build fetches the woff2, serves it
|
|
191
|
+
// same-origin (no third-party request, no FOUT), preloads it, and synthesizes a
|
|
192
|
+
// size-adjusted fallback face so there's no layout shift. globals.css reads it
|
|
193
|
+
// via `var(--font-sans, …)`; layout.tsx carries no font <link>.
|
|
194
|
+
fonts: [
|
|
195
|
+
font({
|
|
196
|
+
family: "Inter",
|
|
197
|
+
variable: "--font-sans",
|
|
198
|
+
weights: ["400", "500", "600", "700"],
|
|
199
|
+
subsets: ["latin"],
|
|
200
|
+
display: "swap",
|
|
201
|
+
preload: true,
|
|
202
|
+
}),
|
|
203
|
+
],
|
|
189
204
|
routes: await discoverAppRoutes(),
|
|
190
205
|
});
|
|
191
206
|
|
|
@@ -1,6 +1,5 @@
|
|
|
1
|
-
//
|
|
2
|
-
//
|
|
3
|
-
// existing imports (`@/lib/products`) keep working. Edit lib/site.config.ts.
|
|
1
|
+
// Re-exports the marketing products from the single site config so existing
|
|
2
|
+
// `@/lib/products` imports keep working. Edit lib/site.config.ts.
|
|
4
3
|
export {
|
|
5
4
|
PRODUCTS,
|
|
6
5
|
productBySlug,
|
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
// THE single source of truth for everything business-specific in this template.
|
|
2
2
|
// Rebrand the entire site by editing this ONE file — the marketing components
|
|
3
3
|
// (hero, pricing, FAQ, footer, nav) all read from here and stay generic. The
|
|
4
|
-
// `create-pylon` scaffolder and automated generators target this file too
|
|
5
|
-
// whole site can be themed by producing one typed object.
|
|
4
|
+
// `create-pylon` scaffolder and automated generators target this file too.
|
|
6
5
|
//
|
|
7
6
|
// Colors live here (applied as CSS variables on <html> in app/layout.tsx), so
|
|
8
7
|
// you don't touch globals.css to re-theme the marketing pages.
|
|
@@ -1,7 +1,6 @@
|
|
|
1
|
-
//
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
// working. Edit lib/site.config.ts.
|
|
1
|
+
// Re-exports the marketing content (solutions, resources, company, comparisons)
|
|
2
|
+
// from the single site config so existing `@/lib/site` imports keep working.
|
|
3
|
+
// Edit lib/site.config.ts.
|
|
5
4
|
export {
|
|
6
5
|
SOLUTIONS,
|
|
7
6
|
RESOURCES,
|
|
@@ -13,15 +13,15 @@ import {
|
|
|
13
13
|
// `/api/auth/password/*`), then `persistSession` writes the freshly-minted
|
|
14
14
|
// token to local storage so the sync engine + `callFn` authenticate AS THE
|
|
15
15
|
// OWNER on the next load. This step matters here specifically: the landing page
|
|
16
|
-
// mints an anonymous guest session (for the live
|
|
16
|
+
// mints an anonymous guest session (for the live upvotes), and without
|
|
17
17
|
// persisting the real session that stale guest token would shadow the owner's
|
|
18
|
-
// — so the owner-only `
|
|
18
|
+
// — so the owner-only `submissionsForOwner` call would come back as a guest and
|
|
19
19
|
// get rejected. We then do a full navigation to /dashboard so the SSR runtime
|
|
20
20
|
// re-resolves auth from the HttpOnly cookie and renders server-side.
|
|
21
21
|
//
|
|
22
|
-
// A
|
|
23
|
-
// their one account. Whoever signs in only sees data if their email
|
|
24
|
-
// PYLON_OWNER_EMAIL — enforced by the
|
|
22
|
+
// A directory is single-tenant: there's no public sign-up, just the owner
|
|
23
|
+
// creating their one account. Whoever signs in only sees data if their email
|
|
24
|
+
// matches PYLON_OWNER_EMAIL — enforced by the submissionsForOwner function.
|
|
25
25
|
export function AuthForm() {
|
|
26
26
|
const [mode, setMode] = useState<"login" | "register">("login");
|
|
27
27
|
const [email, setEmail] = useState("");
|
|
@@ -139,7 +139,14 @@
|
|
|
139
139
|
body {
|
|
140
140
|
background-color: var(--color-background);
|
|
141
141
|
color: var(--color-foreground);
|
|
142
|
-
font-family:
|
|
142
|
+
font-family: var(
|
|
143
|
+
--font-sans,
|
|
144
|
+
Inter,
|
|
145
|
+
ui-sans-serif,
|
|
146
|
+
system-ui,
|
|
147
|
+
-apple-system,
|
|
148
|
+
sans-serif
|
|
149
|
+
);
|
|
143
150
|
-webkit-font-smoothing: antialiased;
|
|
144
151
|
}
|
|
145
152
|
button {
|
|
@@ -45,12 +45,10 @@ export default function RootLayout({ children, url, auth }: LayoutProps) {
|
|
|
45
45
|
<meta charSet="utf-8" />
|
|
46
46
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
47
47
|
{/* No <title> here — each page's exported `metadata` sets it. */}
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap"
|
|
53
|
-
/>
|
|
48
|
+
{/* Inter is declared in app.ts (fonts: [...]) and self-hosted by the
|
|
49
|
+
build — the runtime injects @font-face + <link rel=preload> + a
|
|
50
|
+
size-adjusted fallback here automatically. No third-party request,
|
|
51
|
+
no layout shift; change the family in app.ts. */}
|
|
54
52
|
{/* Tailwind is compiled by Pylon from app/globals.css and injected here. */}
|
|
55
53
|
</head>
|
|
56
54
|
<body className="flex min-h-screen flex-col bg-background text-foreground antialiased">
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { Sitemap } from "@pylonsync/react";
|
|
2
2
|
|
|
3
3
|
// app/sitemap.ts → served at /sitemap.xml. Point SITE_URL at your domain in
|
|
4
|
-
// production.
|
|
4
|
+
// production. This lists the public homepage; add more URLs as the site grows.
|
|
5
5
|
const SITE = process.env.SITE_URL ?? "http://localhost:4321";
|
|
6
6
|
|
|
7
7
|
export default async function sitemap(): Promise<Sitemap> {
|
|
@@ -5,6 +5,7 @@ import {
|
|
|
5
5
|
auth,
|
|
6
6
|
buildManifest,
|
|
7
7
|
discoverAppRoutes,
|
|
8
|
+
font,
|
|
8
9
|
} from "@pylonsync/sdk";
|
|
9
10
|
|
|
10
11
|
// ---------------------------------------------------------------------------
|
|
@@ -138,6 +139,20 @@ const manifest = buildManifest({
|
|
|
138
139
|
actions: [],
|
|
139
140
|
policies: [listingPolicy, submissionPolicy, userPolicy],
|
|
140
141
|
auth: auth(),
|
|
142
|
+
// Self-hosted Inter (next/font parity): the build fetches the woff2, serves it
|
|
143
|
+
// same-origin (no third-party request, no FOUT), preloads it, and synthesizes a
|
|
144
|
+
// size-adjusted fallback face so there's no layout shift. globals.css reads it
|
|
145
|
+
// via `var(--font-sans, …)`; layout.tsx carries no font <link>.
|
|
146
|
+
fonts: [
|
|
147
|
+
font({
|
|
148
|
+
family: "Inter",
|
|
149
|
+
variable: "--font-sans",
|
|
150
|
+
weights: ["400", "500", "600", "700"],
|
|
151
|
+
subsets: ["latin"],
|
|
152
|
+
display: "swap",
|
|
153
|
+
preload: true,
|
|
154
|
+
}),
|
|
155
|
+
],
|
|
141
156
|
routes: await discoverAppRoutes(),
|
|
142
157
|
});
|
|
143
158
|
|
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
// Who owns this
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
1
|
+
// Who owns this directory? A directory is single-tenant — one curator — so
|
|
2
|
+
// ownership is just "the email the owner signs in with", configured once via
|
|
3
|
+
// the PYLON_OWNER_EMAIL env var. The owner-only functions (submissionsForOwner
|
|
4
|
+
// etc.) read that env (via `ctx.env`) and compare it here.
|
|
5
5
|
//
|
|
6
6
|
// Fail closed: if PYLON_OWNER_EMAIL is unset, NOBODY is the owner and the
|
|
7
7
|
// dashboard stays locked. That's deliberate — an unset owner on a public site
|
|
8
|
-
// must not mean "everyone can read the
|
|
8
|
+
// must not mean "everyone can read the submissions". Set it in .env (see
|
|
9
9
|
// .env.example) before signing in.
|
|
10
10
|
|
|
11
11
|
export function normalizeOwner(raw: string | null | undefined): string | null {
|
|
@@ -62,9 +62,8 @@ export default function App() {
|
|
|
62
62
|
}
|
|
63
63
|
|
|
64
64
|
function Chat() {
|
|
65
|
-
// Live subscriptions — sync engine pushes diffs over WebSocket
|
|
66
|
-
//
|
|
67
|
-
// re-render this component without polling.
|
|
65
|
+
// Live subscriptions — the sync engine pushes diffs over WebSocket, so new
|
|
66
|
+
// rooms/messages from any device re-render this component without polling.
|
|
68
67
|
const { data: rooms = [] } = db.useQuery<Room>("Room", {
|
|
69
68
|
orderBy: { createdAt: "asc" },
|
|
70
69
|
});
|
|
@@ -13,15 +13,15 @@ import {
|
|
|
13
13
|
// `/api/auth/password/*`), then `persistSession` writes the freshly-minted
|
|
14
14
|
// token to local storage so the sync engine + `callFn` authenticate AS THE
|
|
15
15
|
// OWNER on the next load. This step matters here specifically: the landing page
|
|
16
|
-
// mints an anonymous guest session (for the live
|
|
16
|
+
// mints an anonymous guest session (for the live booking picker), and without
|
|
17
17
|
// persisting the real session that stale guest token would shadow the owner's
|
|
18
|
-
// — so the owner-only `
|
|
18
|
+
// — so the owner-only `bookingsForOwner` call would come back as a guest and get
|
|
19
19
|
// rejected. We then do a full navigation to /dashboard so the SSR runtime
|
|
20
20
|
// re-resolves auth from the HttpOnly cookie and renders server-side.
|
|
21
21
|
//
|
|
22
|
-
// A
|
|
22
|
+
// A booking site is single-tenant: there's no public signup funnel, just the owner
|
|
23
23
|
// creating their one account. Whoever signs in only sees data if their email
|
|
24
|
-
// matches PYLON_OWNER_EMAIL — enforced by the
|
|
24
|
+
// matches PYLON_OWNER_EMAIL — enforced by the bookingsForOwner function.
|
|
25
25
|
export function AuthForm() {
|
|
26
26
|
const [mode, setMode] = useState<"login" | "signup">("login");
|
|
27
27
|
const [email, setEmail] = useState("");
|
|
@@ -139,7 +139,14 @@
|
|
|
139
139
|
body {
|
|
140
140
|
background-color: var(--color-background);
|
|
141
141
|
color: var(--color-foreground);
|
|
142
|
-
font-family:
|
|
142
|
+
font-family: var(
|
|
143
|
+
--font-sans,
|
|
144
|
+
Inter,
|
|
145
|
+
ui-sans-serif,
|
|
146
|
+
system-ui,
|
|
147
|
+
-apple-system,
|
|
148
|
+
sans-serif
|
|
149
|
+
);
|
|
143
150
|
-webkit-font-smoothing: antialiased;
|
|
144
151
|
}
|
|
145
152
|
button {
|
|
@@ -39,12 +39,10 @@ export default function RootLayout({ children, url, auth }: LayoutProps) {
|
|
|
39
39
|
<head>
|
|
40
40
|
<meta charSet="utf-8" />
|
|
41
41
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap"
|
|
47
|
-
/>
|
|
42
|
+
{/* Inter is declared in app.ts (fonts: [...]) and self-hosted by the
|
|
43
|
+
build — the runtime injects @font-face + <link rel=preload> + a
|
|
44
|
+
size-adjusted fallback here automatically. No third-party request,
|
|
45
|
+
no layout shift; change the family in app.ts. */}
|
|
48
46
|
</head>
|
|
49
47
|
<body className="flex min-h-screen flex-col bg-background text-foreground antialiased">
|
|
50
48
|
<SectionScroller />
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { Sitemap } from "@pylonsync/react";
|
|
2
2
|
|
|
3
3
|
// app/sitemap.ts → served at /sitemap.xml. Point SITE_URL at your domain in
|
|
4
|
-
// production.
|
|
4
|
+
// production. This site is a single public page, so the sitemap is just "/".
|
|
5
5
|
const SITE = process.env.SITE_URL ?? "http://localhost:4321";
|
|
6
6
|
|
|
7
7
|
export default async function sitemap(): Promise<Sitemap> {
|
|
@@ -5,6 +5,7 @@ import {
|
|
|
5
5
|
auth,
|
|
6
6
|
buildManifest,
|
|
7
7
|
discoverAppRoutes,
|
|
8
|
+
font,
|
|
8
9
|
} from "@pylonsync/sdk";
|
|
9
10
|
|
|
10
11
|
// ---------------------------------------------------------------------------
|
|
@@ -123,6 +124,20 @@ const manifest = buildManifest({
|
|
|
123
124
|
actions: [],
|
|
124
125
|
policies: [bookingPolicy, bookedSlotPolicy, userPolicy],
|
|
125
126
|
auth: auth(),
|
|
127
|
+
// Self-hosted Inter (next/font parity): the build fetches the woff2, serves it
|
|
128
|
+
// same-origin (no third-party request, no FOUT), preloads it, and synthesizes a
|
|
129
|
+
// size-adjusted fallback face so there's no layout shift. globals.css reads it
|
|
130
|
+
// via `var(--font-sans, …)`; layout.tsx carries no font <link>.
|
|
131
|
+
fonts: [
|
|
132
|
+
font({
|
|
133
|
+
family: "Inter",
|
|
134
|
+
variable: "--font-sans",
|
|
135
|
+
weights: ["400", "500", "600", "700"],
|
|
136
|
+
subsets: ["latin"],
|
|
137
|
+
display: "swap",
|
|
138
|
+
preload: true,
|
|
139
|
+
}),
|
|
140
|
+
],
|
|
126
141
|
routes: await discoverAppRoutes(),
|
|
127
142
|
});
|
|
128
143
|
|
|
@@ -95,10 +95,8 @@ export function initials(name: string) {
|
|
|
95
95
|
.toUpperCase();
|
|
96
96
|
}
|
|
97
97
|
|
|
98
|
-
// A deliberately-obvious image placeholder
|
|
99
|
-
//
|
|
100
|
-
// "swap this" instruction telling you exactly what to replace and where. Looks
|
|
101
|
-
// tidy enough to demo, but no one will mistake it for a finished design.
|
|
98
|
+
// A deliberately-obvious image placeholder — dashed border, a photo glyph, and
|
|
99
|
+
// a one-line "swap this" instruction. Real sites drop a photo here.
|
|
102
100
|
//
|
|
103
101
|
// shape — "landscape" | "portrait" | "square" | "circle"
|
|
104
102
|
// title — what photo belongs here ("A photo of your shop")
|
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
// Who owns this
|
|
1
|
+
// Who owns this site? A booking site is single-tenant — one business, one
|
|
2
2
|
// owner — so ownership is just "the email the owner signs in with", configured
|
|
3
|
-
// once via the PYLON_OWNER_EMAIL env var. The owner-only `
|
|
3
|
+
// once via the PYLON_OWNER_EMAIL env var. The owner-only `bookingsForOwner`
|
|
4
4
|
// function reads that env (via `ctx.env`) and compares it here.
|
|
5
5
|
//
|
|
6
6
|
// Fail closed: if PYLON_OWNER_EMAIL is unset, NOBODY is the owner and the
|
|
7
7
|
// dashboard stays locked. That's deliberate — an unset owner on a public site
|
|
8
|
-
// must not mean "everyone can read the
|
|
8
|
+
// must not mean "everyone can read the bookings". Set it in .env (see
|
|
9
9
|
// .env.example) before signing in.
|
|
10
10
|
|
|
11
11
|
export function normalizeOwner(raw: string | null | undefined): string | null {
|
|
@@ -1,9 +1,7 @@
|
|
|
1
|
-
// THE single source of truth for everything business-specific.
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
// create-pylon scaffolder and Mast target this file: a whole appointment site
|
|
6
|
-
// is themed + configured by producing one typed object.
|
|
1
|
+
// THE single source of truth for everything business-specific. The landing
|
|
2
|
+
// page, layout, AND the createBooking server function all read from here, so
|
|
3
|
+
// services, prices, weekly hours, and lead time stay in lockstep — rebrand and
|
|
4
|
+
// reconfigure the whole site by editing this ONE file.
|
|
7
5
|
//
|
|
8
6
|
// Colors live here (applied as CSS variables on <html> in app/layout.tsx).
|
|
9
7
|
//
|
|
@@ -18,9 +18,6 @@ import {
|
|
|
18
18
|
type Listing,
|
|
19
19
|
} from "../../../client/market";
|
|
20
20
|
|
|
21
|
-
// Data-driven SEO: the title + description come from the listing itself,
|
|
22
|
-
// fetched on the server. `generateMetadata` is handed the same PageProps as
|
|
23
|
-
// the page (params + serverData), so it reads the row directly.
|
|
24
21
|
// Resolve a listing from the URL segment, which is its slug
|
|
25
22
|
// ("herman-miller-aeron-a1f3"). Falls back to a raw id lookup so older
|
|
26
23
|
// id-shaped links keep working.
|
|
@@ -34,6 +31,9 @@ async function resolveListing(
|
|
|
34
31
|
);
|
|
35
32
|
}
|
|
36
33
|
|
|
34
|
+
// Data-driven SEO: the title + description come from the listing itself,
|
|
35
|
+
// fetched on the server. `generateMetadata` is handed the same PageProps as
|
|
36
|
+
// the page (params + serverData), so it reads the row directly.
|
|
37
37
|
export const generateMetadata: GenerateMetadata = async ({
|
|
38
38
|
params,
|
|
39
39
|
serverData,
|
|
@@ -48,7 +48,6 @@ export default mutation<MakeOfferArgs, MakeOfferResult>({
|
|
|
48
48
|
throw ctx.error("INVALID_ARGS", "offer must be greater than zero");
|
|
49
49
|
|
|
50
50
|
const id = await ctx.db.insert("Offer", {
|
|
51
|
-
// Reuse the optimistic ghost's id so the broadcast merges in place.
|
|
52
51
|
id: args._optimisticId,
|
|
53
52
|
listingId: args.listingId,
|
|
54
53
|
listingTitle: listing.title,
|
|
@@ -13,15 +13,15 @@ import {
|
|
|
13
13
|
// `/api/auth/password/*`), then `persistSession` writes the freshly-minted
|
|
14
14
|
// token to local storage so the sync engine + `callFn` authenticate AS THE
|
|
15
15
|
// OWNER on the next load. This step matters here specifically: the landing page
|
|
16
|
-
// mints an anonymous guest session (for the live
|
|
16
|
+
// mints an anonymous guest session (for the live table picker), and without
|
|
17
17
|
// persisting the real session that stale guest token would shadow the owner's
|
|
18
|
-
// — so the owner-only `
|
|
19
|
-
// rejected. We then do a full navigation to /dashboard so the SSR runtime
|
|
18
|
+
// — so the owner-only `reservationsForOwner` call would come back as a guest and
|
|
19
|
+
// get rejected. We then do a full navigation to /dashboard so the SSR runtime
|
|
20
20
|
// re-resolves auth from the HttpOnly cookie and renders server-side.
|
|
21
21
|
//
|
|
22
|
-
//
|
|
22
|
+
// This site is single-tenant: there's no public signup funnel, just the owner
|
|
23
23
|
// creating their one account. Whoever signs in only sees data if their email
|
|
24
|
-
// matches PYLON_OWNER_EMAIL — enforced by the
|
|
24
|
+
// matches PYLON_OWNER_EMAIL — enforced by the reservationsForOwner function.
|
|
25
25
|
export function AuthForm() {
|
|
26
26
|
const [mode, setMode] = useState<"login" | "signup">("login");
|
|
27
27
|
const [email, setEmail] = useState("");
|
|
@@ -139,7 +139,14 @@
|
|
|
139
139
|
body {
|
|
140
140
|
background-color: var(--color-background);
|
|
141
141
|
color: var(--color-foreground);
|
|
142
|
-
font-family:
|
|
142
|
+
font-family: var(
|
|
143
|
+
--font-sans,
|
|
144
|
+
Inter,
|
|
145
|
+
ui-sans-serif,
|
|
146
|
+
system-ui,
|
|
147
|
+
-apple-system,
|
|
148
|
+
sans-serif
|
|
149
|
+
);
|
|
143
150
|
-webkit-font-smoothing: antialiased;
|
|
144
151
|
}
|
|
145
152
|
button {
|
|
@@ -39,12 +39,10 @@ export default function RootLayout({ children, url, auth }: LayoutProps) {
|
|
|
39
39
|
<head>
|
|
40
40
|
<meta charSet="utf-8" />
|
|
41
41
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap"
|
|
47
|
-
/>
|
|
42
|
+
{/* Inter is declared in app.ts (fonts: [...]) and self-hosted by the
|
|
43
|
+
build — the runtime injects @font-face + <link rel=preload> + a
|
|
44
|
+
size-adjusted fallback here automatically. No third-party request,
|
|
45
|
+
no layout shift; change the family in app.ts. */}
|
|
48
46
|
</head>
|
|
49
47
|
<body className="flex min-h-screen flex-col bg-background text-foreground antialiased">
|
|
50
48
|
<SectionScroller />
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { Sitemap } from "@pylonsync/react";
|
|
2
2
|
|
|
3
3
|
// app/sitemap.ts → served at /sitemap.xml. Point SITE_URL at your domain in
|
|
4
|
-
// production. The
|
|
4
|
+
// production. The site is a single public page, so the sitemap is just "/".
|
|
5
5
|
const SITE = process.env.SITE_URL ?? "http://localhost:4321";
|
|
6
6
|
|
|
7
7
|
export default async function sitemap(): Promise<Sitemap> {
|
|
@@ -5,6 +5,7 @@ import {
|
|
|
5
5
|
auth,
|
|
6
6
|
buildManifest,
|
|
7
7
|
discoverAppRoutes,
|
|
8
|
+
font,
|
|
8
9
|
} from "@pylonsync/sdk";
|
|
9
10
|
|
|
10
11
|
// ---------------------------------------------------------------------------
|
|
@@ -107,6 +108,20 @@ const manifest = buildManifest({
|
|
|
107
108
|
actions: [],
|
|
108
109
|
policies: [reservationPolicy, reservationSlotPolicy, userPolicy],
|
|
109
110
|
auth: auth(),
|
|
111
|
+
// Self-hosted Inter (next/font parity): the build fetches the woff2, serves it
|
|
112
|
+
// same-origin (no third-party request, no FOUT), preloads it, and synthesizes a
|
|
113
|
+
// size-adjusted fallback face so there's no layout shift. globals.css reads it
|
|
114
|
+
// via `var(--font-sans, …)`; layout.tsx carries no font <link>.
|
|
115
|
+
fonts: [
|
|
116
|
+
font({
|
|
117
|
+
family: "Inter",
|
|
118
|
+
variable: "--font-sans",
|
|
119
|
+
weights: ["400", "500", "600", "700"],
|
|
120
|
+
subsets: ["latin"],
|
|
121
|
+
display: "swap",
|
|
122
|
+
preload: true,
|
|
123
|
+
}),
|
|
124
|
+
],
|
|
110
125
|
routes: await discoverAppRoutes(),
|
|
111
126
|
});
|
|
112
127
|
|
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
// Who owns this
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
1
|
+
// Who owns this restaurant site? It's single-tenant — one venue, one owner —
|
|
2
|
+
// so ownership is just "the email the owner signs in with", configured once via
|
|
3
|
+
// the PYLON_OWNER_EMAIL env var. The owner-only `reservationsForOwner` function
|
|
4
|
+
// reads that env (via `ctx.env`) and compares it here.
|
|
5
5
|
//
|
|
6
6
|
// Fail closed: if PYLON_OWNER_EMAIL is unset, NOBODY is the owner and the
|
|
7
7
|
// dashboard stays locked. That's deliberate — an unset owner on a public site
|
|
8
|
-
// must not mean "everyone can read the
|
|
8
|
+
// must not mean "everyone can read the reservations". Set it in .env (see
|
|
9
9
|
// .env.example) before signing in.
|
|
10
10
|
|
|
11
11
|
export function normalizeOwner(raw: string | null | undefined): string | null {
|
|
@@ -12,16 +12,16 @@ import {
|
|
|
12
12
|
// auth API directly (`passwordLogin` / `passwordRegister` POST to
|
|
13
13
|
// `/api/auth/password/*`), then `persistSession` writes the freshly-minted
|
|
14
14
|
// token to local storage so the sync engine + `callFn` authenticate AS THE
|
|
15
|
-
// OWNER on the next load. This step matters here specifically: the
|
|
16
|
-
// mints an anonymous guest session (for the live
|
|
15
|
+
// OWNER on the next load. This step matters here specifically: the storefront
|
|
16
|
+
// mints an anonymous guest session (for the live stock grid), and without
|
|
17
17
|
// persisting the real session that stale guest token would shadow the owner's
|
|
18
|
-
// — so the owner-only `
|
|
18
|
+
// — so the owner-only `ordersForOwner` call would come back as a guest and get
|
|
19
19
|
// rejected. We then do a full navigation to /dashboard so the SSR runtime
|
|
20
20
|
// re-resolves auth from the HttpOnly cookie and renders server-side.
|
|
21
21
|
//
|
|
22
|
-
// A
|
|
22
|
+
// A shop is single-tenant: there's no public signup funnel, just the owner
|
|
23
23
|
// creating their one account. Whoever signs in only sees data if their email
|
|
24
|
-
// matches PYLON_OWNER_EMAIL — enforced by the
|
|
24
|
+
// matches PYLON_OWNER_EMAIL — enforced by the ordersForOwner function.
|
|
25
25
|
export function AuthForm() {
|
|
26
26
|
const [mode, setMode] = useState<"login" | "signup">("login");
|
|
27
27
|
const [email, setEmail] = useState("");
|
|
@@ -139,7 +139,14 @@
|
|
|
139
139
|
body {
|
|
140
140
|
background-color: var(--color-background);
|
|
141
141
|
color: var(--color-foreground);
|
|
142
|
-
font-family:
|
|
142
|
+
font-family: var(
|
|
143
|
+
--font-sans,
|
|
144
|
+
Inter,
|
|
145
|
+
ui-sans-serif,
|
|
146
|
+
system-ui,
|
|
147
|
+
-apple-system,
|
|
148
|
+
sans-serif
|
|
149
|
+
);
|
|
143
150
|
-webkit-font-smoothing: antialiased;
|
|
144
151
|
}
|
|
145
152
|
button {
|
|
@@ -15,7 +15,7 @@ interface LayoutProps {
|
|
|
15
15
|
}
|
|
16
16
|
|
|
17
17
|
export default function RootLayout({ children, url, auth }: LayoutProps) {
|
|
18
|
-
// A guest session (minted by <EnsureGuest> for the live
|
|
18
|
+
// A guest session (minted by <EnsureGuest> for the live stock grid) has a
|
|
19
19
|
// `guest_…` user id — that's an anonymous visitor, NOT the signed-in owner,
|
|
20
20
|
// so it shouldn't flip the nav to "Dashboard".
|
|
21
21
|
const signedIn = Boolean(auth?.user_id && !auth.user_id.startsWith("guest_"));
|
|
@@ -45,12 +45,10 @@ export default function RootLayout({ children, url, auth }: LayoutProps) {
|
|
|
45
45
|
<meta charSet="utf-8" />
|
|
46
46
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
47
47
|
{/* No <title> here — each page's exported `metadata` sets it. */}
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap"
|
|
53
|
-
/>
|
|
48
|
+
{/* Inter is declared in app.ts (fonts: [...]) and self-hosted by the
|
|
49
|
+
build — the runtime injects @font-face + <link rel=preload> + a
|
|
50
|
+
size-adjusted fallback here automatically. No third-party request,
|
|
51
|
+
no layout shift; change the family in app.ts. */}
|
|
54
52
|
{/* Tailwind is compiled by Pylon from app/globals.css and injected here. */}
|
|
55
53
|
</head>
|
|
56
54
|
<body className="flex min-h-screen flex-col bg-background text-foreground antialiased">
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { Sitemap } from "@pylonsync/react";
|
|
2
2
|
|
|
3
3
|
// app/sitemap.ts → served at /sitemap.xml. Point SITE_URL at your domain in
|
|
4
|
-
// production. The
|
|
4
|
+
// production. The storefront is a single public page, so the sitemap is just "/".
|
|
5
5
|
const SITE = process.env.SITE_URL ?? "http://localhost:4321";
|
|
6
6
|
|
|
7
7
|
export default async function sitemap(): Promise<Sitemap> {
|