@pylonsync/create-pylon 0.11.1 → 0.11.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/create-pylon.js +1 -1
- package/package.json +1 -1
- package/templates/_root/AGENTS.md +1 -1
- package/templates/agency/AGENTS.md +1 -1
- package/templates/agency/app/(marketing)/contact-form.tsx +22 -32
- package/templates/agency/app/(marketing)/layout.tsx +36 -64
- package/templates/agency/app/(marketing)/not-found.tsx +6 -8
- package/templates/agency/app/(marketing)/page.tsx +93 -144
- package/templates/agency/app/(marketing)/work/[slug]/page.tsx +69 -71
- package/templates/agency/app/(marketing)/work/page.tsx +20 -17
- package/templates/agency/app/globals.css +15 -6
- package/templates/agency/app.ts +10 -0
- package/templates/agency/components/marketing.tsx +40 -104
- package/templates/agency/lib/site.config.ts +34 -52
- package/templates/agency/public/images/hero.jpg +0 -0
- package/templates/agency/public/images/team/claire-donovan.jpg +0 -0
- package/templates/agency/public/images/team/dana-okafor.jpg +0 -0
- package/templates/agency/public/images/team/marcus-lee.jpg +0 -0
- package/templates/agency/public/images/work/atlas-health.jpg +0 -0
- package/templates/agency/public/images/work/cohort.jpg +0 -0
- package/templates/agency/public/images/work/ledger.jpg +0 -0
- package/templates/agency/public/images/work/vela.jpg +0 -0
- package/templates/ai-chat/AGENTS.md +1 -1
- package/templates/ai-studio/AGENTS.md +1 -1
- package/templates/barebones/AGENTS.md +1 -1
- package/templates/chat/AGENTS.md +1 -1
- package/templates/consumer/AGENTS.md +1 -1
- package/templates/consumer/README.md +39 -23
- package/templates/consumer/app/(app)/explore/page.tsx +13 -0
- package/templates/consumer/app/(app)/layout.tsx +17 -0
- package/templates/consumer/app/(app)/p/[id]/page.tsx +13 -0
- package/templates/consumer/app/(app)/page.tsx +15 -0
- package/templates/consumer/app/(app)/u/[username]/page.tsx +14 -0
- package/templates/consumer/app/error.tsx +1 -1
- package/templates/consumer/app/globals.css +25 -0
- package/templates/consumer/app/layout.tsx +3 -10
- package/templates/consumer/app/llms.ts +10 -10
- package/templates/consumer/app/not-found.tsx +9 -22
- package/templates/consumer/app/robots.ts +3 -4
- package/templates/consumer/app/sitemap.ts +7 -21
- package/templates/consumer/app.ts +152 -33
- package/templates/consumer/components/social/app-chrome.tsx +109 -0
- package/templates/consumer/components/social/avatar.tsx +50 -0
- package/templates/consumer/components/social/comment-form.tsx +51 -0
- package/templates/consumer/components/social/create-post.tsx +155 -0
- package/templates/consumer/components/social/edit-profile.tsx +103 -0
- package/templates/consumer/components/social/explore-screen.tsx +23 -0
- package/templates/consumer/components/social/home-screen.tsx +161 -0
- package/templates/consumer/components/social/post-card.tsx +77 -0
- package/templates/consumer/components/social/post-grid.tsx +46 -0
- package/templates/consumer/components/social/post-parts.tsx +146 -0
- package/templates/consumer/components/social/post-screen.tsx +173 -0
- package/templates/consumer/components/social/profile-screen.tsx +146 -0
- package/templates/consumer/components/social/rich-text.tsx +22 -0
- package/templates/consumer/components/social/session.tsx +43 -0
- package/templates/consumer/components/social/use-social.ts +86 -0
- package/templates/consumer/functions/addComment.ts +27 -0
- package/templates/consumer/functions/createPost.ts +39 -0
- package/templates/consumer/functions/ensureProfile.ts +39 -0
- package/templates/consumer/functions/seedFeed.ts +38 -0
- package/templates/consumer/functions/updateProfile.ts +61 -0
- package/templates/consumer/lib/seed.ts +192 -0
- package/templates/consumer/lib/site.ts +6 -0
- package/templates/consumer/lib/social.ts +245 -0
- package/templates/consumer/tests/social.test.ts +157 -0
- package/templates/creator/AGENTS.md +1 -1
- package/templates/creator/app/(marketing)/layout.tsx +35 -69
- package/templates/creator/app/(marketing)/newsletter-signup.tsx +15 -26
- package/templates/creator/app/(marketing)/not-found.tsx +8 -6
- package/templates/creator/app/(marketing)/page.tsx +90 -106
- package/templates/creator/app/globals.css +14 -0
- package/templates/creator/app/layout.tsx +1 -0
- package/templates/creator/app.ts +10 -0
- package/templates/creator/components/marketing.tsx +28 -89
- package/templates/creator/lib/site.config.ts +51 -29
- package/templates/creator/public/images/headshot.jpg +0 -0
- package/templates/crm/AGENTS.md +1 -1
- package/templates/crm/lib/seed.ts +153 -21
- package/templates/directory/AGENTS.md +1 -1
- package/templates/directory/app/(marketing)/directory-browse.tsx +99 -137
- package/templates/directory/app/(marketing)/layout.tsx +37 -88
- package/templates/directory/app/(marketing)/not-found.tsx +11 -10
- package/templates/directory/app/(marketing)/page.tsx +12 -33
- package/templates/directory/app/(marketing)/submit/page.tsx +9 -10
- package/templates/directory/app/(marketing)/submit-form.tsx +26 -26
- package/templates/directory/app/globals.css +15 -4
- package/templates/directory/app.ts +10 -0
- package/templates/directory/components/marketing.tsx +27 -91
- package/templates/directory/lib/site.config.ts +13 -24
- package/templates/helpdesk/AGENTS.md +1 -1
- package/templates/helpdesk/lib/seed.ts +188 -104
- package/templates/inventory/AGENTS.md +1 -1
- package/templates/inventory/lib/seed.ts +175 -44
- package/templates/invoices/AGENTS.md +1 -1
- package/templates/invoices/lib/seed.ts +74 -89
- package/templates/local-service/AGENTS.md +1 -1
- package/templates/local-service/app/(marketing)/booking-widget.tsx +31 -31
- package/templates/local-service/app/(marketing)/layout.tsx +25 -37
- package/templates/local-service/app/(marketing)/not-found.tsx +4 -4
- package/templates/local-service/app/(marketing)/page.tsx +145 -139
- package/templates/local-service/app/globals.css +11 -0
- package/templates/local-service/app/layout.tsx +2 -0
- package/templates/local-service/app.ts +9 -0
- package/templates/local-service/components/marketing.tsx +14 -39
- package/templates/local-service/lib/site.config.ts +16 -23
- package/templates/local-service/public/images/hero.jpg +0 -0
- package/templates/marketplace/AGENTS.md +1 -1
- package/templates/marketplace/client/SellForm.tsx +2 -0
- package/templates/projects/AGENTS.md +1 -1
- package/templates/projects/lib/seed.ts +103 -32
- package/templates/restaurant/AGENTS.md +1 -1
- package/templates/restaurant/app/(marketing)/layout.tsx +26 -48
- package/templates/restaurant/app/(marketing)/page.tsx +146 -123
- package/templates/restaurant/app/globals.css +11 -0
- package/templates/restaurant/app/layout.tsx +1 -0
- package/templates/restaurant/app.ts +10 -0
- package/templates/restaurant/components/marketing.tsx +10 -0
- package/templates/restaurant/lib/site.config.ts +16 -14
- package/templates/restaurant/public/images/bar.jpg +0 -0
- package/templates/restaurant/public/images/branzino.jpg +0 -0
- package/templates/restaurant/public/images/hearth.jpg +0 -0
- package/templates/restaurant/public/images/hero.jpg +0 -0
- package/templates/saas/AGENTS.md +1 -1
- package/templates/shop/AGENTS.md +1 -1
- package/templates/shop/public/images/products/cedar-smoke.jpg +0 -0
- package/templates/shop/public/images/products/linen.jpg +0 -0
- package/templates/todo/AGENTS.md +1 -1
- package/templates/waitlist/AGENTS.md +1 -1
- package/templates/waitlist/app/(marketing)/layout.tsx +39 -85
- package/templates/waitlist/app/(marketing)/not-found.tsx +8 -9
- package/templates/waitlist/app/(marketing)/page.tsx +51 -86
- package/templates/waitlist/app/(marketing)/waitlist-hero.tsx +43 -83
- package/templates/waitlist/app/globals.css +23 -0
- package/templates/waitlist/app/layout.tsx +1 -0
- package/templates/waitlist/app.ts +18 -0
- package/templates/waitlist/components/marketing.tsx +67 -81
- package/templates/waitlist/lib/site.config.ts +52 -59
- package/templates/consumer/app/feed-client.tsx +0 -159
- package/templates/consumer/app/page.tsx +0 -27
- package/templates/consumer/functions/_keep.ts +0 -13
- package/templates/consumer/functions/seedPosts.ts +0 -56
- package/templates/consumer/tests/example.test.ts +0 -12
package/bin/create-pylon.js
CHANGED
|
@@ -106,7 +106,7 @@ const TEMPLATE_REGISTRY = {
|
|
|
106
106
|
},
|
|
107
107
|
consumer: {
|
|
108
108
|
blurb:
|
|
109
|
-
"
|
|
109
|
+
"Photo sharing — feed, explore, post pages, profiles, follows, likes, comments, and saves. One SSR app.",
|
|
110
110
|
platforms: [],
|
|
111
111
|
unified: true,
|
|
112
112
|
},
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pylonsync/create-pylon",
|
|
3
|
-
"version": "0.11.
|
|
3
|
+
"version": "0.11.3",
|
|
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"
|
|
@@ -57,7 +57,7 @@ this pass before the user has to ask.
|
|
|
57
57
|
- **`ctx.llm.stream(request, onEvent)` streams tokens as they generate** and still resolves with the full response, so `stop_reason === "tool_use"` drives an agent tool loop. Events are `text_delta` / `tool_use_start` / `tool_input_delta` / `done`. Same auth + model-allowlist gating as `ctx.llm.complete`. Streaming does NOT extend the call deadline (`PYLON_FN_CALL_TIMEOUT`, 30s default) — set `timeout: <secs>` on the def for a long run.
|
|
58
58
|
- **`ctx.stream.write(text)` streams to the caller — and every fn stream is RESUMABLE**: the server buffers frames under a stream id (`streamFn`'s `onStreamId`), so a dropped connection or closed tab catches up via `resumeStream(id)`, including the final result after the handler finished. `ctx.rooms.broadcast(room, topic, data)` fans out to every CURRENTLY-CONNECTED subscriber (second device, second tab) but does not replay missed messages — use the stream id for anything that must survive a gap. Broadcast resolves `{ delivered: false }` when the room has no members — a no-op, not an error. Clients receive with `useRoomMessages(room, cb)` from `@pylonsync/react`; `useRoom` is the send side.
|
|
59
59
|
- **It's `db.useQueryOne`, not `useOne`.** Validators and field types have aliases: `v.bool`/`v.boolean`, `v.float`/`v.number`.
|
|
60
|
-
- **There is no `ctx.files` or `defineWorkflow`/`defineJob`.** Files go through `<FileUpload>` + `/api/files/*`; deferred execution is `ctx.scheduler.runAfter/runAt/cancel`.
|
|
60
|
+
- **There is no `ctx.files` or `defineWorkflow`/`defineJob`.** Files go through `<FileUpload>` + `/api/files/*`; an upload is readable by its uploader only, so pass `visibility: "public"` to `uploadFile` or `<FileUpload>` for files every visitor sees; deferred execution is `ctx.scheduler.runAfter/runAt/cancel`.
|
|
61
61
|
|
|
62
62
|
## Use the CLI
|
|
63
63
|
|
|
@@ -65,7 +65,7 @@ this pass before the user has to ask.
|
|
|
65
65
|
- **`ctx.llm.stream(request, onEvent)` streams tokens as they generate** and still resolves with the full response, so `stop_reason === "tool_use"` drives an agent tool loop. Events are `text_delta` / `tool_use_start` / `tool_input_delta` / `done`. Same auth + model-allowlist gating as `ctx.llm.complete`. Streaming does NOT extend the call deadline (`PYLON_FN_CALL_TIMEOUT`, 30s default) — set `timeout: <secs>` on the def for a long run.
|
|
66
66
|
- **`ctx.stream.write(text)` streams to the caller — and every fn stream is RESUMABLE**: the server buffers frames under a stream id (`streamFn`'s `onStreamId`), so a dropped connection or closed tab catches up via `resumeStream(id)`, including the final result after the handler finished. `ctx.rooms.broadcast(room, topic, data)` fans out to every CURRENTLY-CONNECTED subscriber (second device, second tab) but does not replay missed messages — use the stream id for anything that must survive a gap. Broadcast resolves `{ delivered: false }` when the room has no members — a no-op, not an error. Clients receive with `useRoomMessages(room, cb)` from `@pylonsync/react`; `useRoom` is the send side.
|
|
67
67
|
- **It's `db.useQueryOne`, not `useOne`.** Validators and field types have aliases: `v.bool`/`v.boolean`, `v.float`/`v.number`.
|
|
68
|
-
- **Use the supported file and scheduling APIs.** Files go through `<FileUpload>` and `/api/files/*`; there is no `ctx.files`. One-shot work uses `ctx.scheduler.runAfter`, `runAt`, or `cancel`; there is no `defineWorkflow` or `defineJob`. Recurring work uses `cron("0 * * * *", "fnName")` in `buildManifest({ crons: [...] })`, imported from `@pylonsync/sdk`. Make the target function `internal: true`. It runs with anonymous auth, but its own `ctx.db.*` calls are server-side and bypass policies. Use `ctx.auth.elevate({ admin: true, reason: "..." })`, with a mandatory reason, only when chaining another internal function through `ctx.scheduler`.
|
|
68
|
+
- **Use the supported file and scheduling APIs.** Files go through `<FileUpload>` and `/api/files/*`; an upload is readable by its uploader only, so pass `visibility: "public"` to `uploadFile` or `<FileUpload>` for files every visitor sees; there is no `ctx.files`. One-shot work uses `ctx.scheduler.runAfter`, `runAt`, or `cancel`; there is no `defineWorkflow` or `defineJob`. Recurring work uses `cron("0 * * * *", "fnName")` in `buildManifest({ crons: [...] })`, imported from `@pylonsync/sdk`. Make the target function `internal: true`. It runs with anonymous auth, but its own `ctx.db.*` calls are server-side and bypass policies. Use `ctx.auth.elevate({ admin: true, reason: "..." })`, with a mandatory reason, only when chaining another internal function through `ctx.scheduler`.
|
|
69
69
|
|
|
70
70
|
## Testing
|
|
71
71
|
|
|
@@ -9,7 +9,7 @@ import { siteConfig } from "@/lib/site.config";
|
|
|
9
9
|
// row via `db.useQuery("Capacity")` — so when the owner books a project from the
|
|
10
10
|
// dashboard, the "N slots open" number drops live in every open tab. No refresh.
|
|
11
11
|
//
|
|
12
|
-
// • <LiveSlots> — the hero
|
|
12
|
+
// • <LiveSlots> — the hero line ("3 project slots open, Q3 2026").
|
|
13
13
|
// • <ContactForm> — the "start a project" form. submitInquiry is a public
|
|
14
14
|
// mutation, so it works for anonymous visitors; the Inquiry
|
|
15
15
|
// it writes is pure PII and can never be read back by a
|
|
@@ -33,7 +33,7 @@ function useSeedCapacity() {
|
|
|
33
33
|
}, []);
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
-
/* ------------------------------ hero
|
|
36
|
+
/* ------------------------------ hero line ------------------------------ */
|
|
37
37
|
|
|
38
38
|
export function LiveSlots() {
|
|
39
39
|
return (
|
|
@@ -65,7 +65,7 @@ function SlotsPill({
|
|
|
65
65
|
const period = label || siteConfig.capacity.label;
|
|
66
66
|
const bookedOut = open <= 0;
|
|
67
67
|
return (
|
|
68
|
-
<span className="inline-flex items-center gap-2
|
|
68
|
+
<span className="inline-flex items-center gap-2.5 text-[14px] text-zinc-700">
|
|
69
69
|
{loading ? (
|
|
70
70
|
<span className="inline-flex size-2 rounded-full bg-zinc-300" />
|
|
71
71
|
) : (
|
|
@@ -82,8 +82,8 @@ function SlotsPill({
|
|
|
82
82
|
<span>Booked through {period}</span>
|
|
83
83
|
) : (
|
|
84
84
|
<span>
|
|
85
|
-
<span className="tabular-nums text-
|
|
86
|
-
{open === 1 ? "project slot" : "project slots"} open
|
|
85
|
+
<span className="tabular-nums text-ink">{open}</span>{" "}
|
|
86
|
+
{open === 1 ? "project slot" : "project slots"} open, {period}
|
|
87
87
|
</span>
|
|
88
88
|
)}
|
|
89
89
|
</span>
|
|
@@ -156,7 +156,7 @@ function FormShell({
|
|
|
156
156
|
setError(
|
|
157
157
|
/valid email|INVALID_ARGS/i.test(msg)
|
|
158
158
|
? "Please enter a valid email address."
|
|
159
|
-
: "Something went wrong
|
|
159
|
+
: "Something went wrong. Try again in a moment.",
|
|
160
160
|
);
|
|
161
161
|
setStatus("idle");
|
|
162
162
|
}
|
|
@@ -164,23 +164,21 @@ function FormShell({
|
|
|
164
164
|
|
|
165
165
|
if (status === "done") {
|
|
166
166
|
return (
|
|
167
|
-
<div className="
|
|
168
|
-
<
|
|
169
|
-
|
|
170
|
-
</div>
|
|
171
|
-
<p className="mt-3 text-[15px] font-semibold text-zinc-900">{contact.confirmationMessage}</p>
|
|
167
|
+
<div className="border-t border-ink pt-6">
|
|
168
|
+
<p className="font-display text-[1.75rem] leading-tight text-ink">Sent.</p>
|
|
169
|
+
<p className="mt-2 text-[15px] leading-relaxed text-zinc-700">{contact.confirmationMessage}</p>
|
|
172
170
|
</div>
|
|
173
171
|
);
|
|
174
172
|
}
|
|
175
173
|
|
|
176
174
|
return (
|
|
177
|
-
<form onSubmit={onSubmit}
|
|
175
|
+
<form onSubmit={onSubmit}>
|
|
178
176
|
{bookedOut ? (
|
|
179
|
-
<p className="mb-
|
|
180
|
-
We
|
|
177
|
+
<p className="mb-5 border-l-2 border-ink pl-3 text-[14px] text-zinc-700">
|
|
178
|
+
We are fully booked{period ? ` through ${period}` : ""}. Send a note anyway and we will write when a slot opens.
|
|
181
179
|
</p>
|
|
182
180
|
) : null}
|
|
183
|
-
<div className="grid gap-
|
|
181
|
+
<div className="grid gap-5 sm:grid-cols-2">
|
|
184
182
|
<Field label="Name" required>
|
|
185
183
|
<input value={form.name} onChange={set("name")} autoComplete="name" aria-label="Name" className={inputCls} disabled={disabled} />
|
|
186
184
|
</Field>
|
|
@@ -199,7 +197,7 @@ function FormShell({
|
|
|
199
197
|
</select>
|
|
200
198
|
</Field>
|
|
201
199
|
</div>
|
|
202
|
-
<div className="mt-
|
|
200
|
+
<div className="mt-5">
|
|
203
201
|
<Field label="Budget">
|
|
204
202
|
<select value={form.budget} onChange={set("budget")} aria-label="Budget" className={inputCls} disabled={disabled}>
|
|
205
203
|
<option value="">Select…</option>
|
|
@@ -209,24 +207,24 @@ function FormShell({
|
|
|
209
207
|
</select>
|
|
210
208
|
</Field>
|
|
211
209
|
</div>
|
|
212
|
-
<div className="mt-
|
|
210
|
+
<div className="mt-5">
|
|
213
211
|
<Field label="What are you building?">
|
|
214
212
|
<textarea
|
|
215
213
|
value={form.message}
|
|
216
214
|
onChange={set("message")}
|
|
217
215
|
rows={4}
|
|
218
216
|
aria-label="Message"
|
|
219
|
-
placeholder="A sentence or two about the project
|
|
220
|
-
className={inputCls + " resize-none py-2.5"}
|
|
217
|
+
placeholder="A sentence or two about the project and the timeline."
|
|
218
|
+
className={inputCls + " h-auto resize-none py-2.5"}
|
|
221
219
|
disabled={disabled}
|
|
222
220
|
/>
|
|
223
221
|
</Field>
|
|
224
222
|
</div>
|
|
225
|
-
{error ? <p className="mt-
|
|
223
|
+
{error ? <p className="mt-4 text-[14px] text-red-700">{error}</p> : null}
|
|
226
224
|
<button
|
|
227
225
|
type="submit"
|
|
228
226
|
disabled={status === "sending" || disabled}
|
|
229
|
-
className="mt-
|
|
227
|
+
className="mt-6 inline-flex h-11 w-full items-center justify-center bg-ink px-7 text-[15px] font-medium text-white transition-colors hover:bg-zinc-700 disabled:opacity-60 sm:w-auto"
|
|
230
228
|
>
|
|
231
229
|
{status === "sending" ? "Sending…" : "Send inquiry"}
|
|
232
230
|
</button>
|
|
@@ -237,9 +235,9 @@ function FormShell({
|
|
|
237
235
|
function Field({ label, required, children }: { label: string; required?: boolean; children: React.ReactNode }) {
|
|
238
236
|
return (
|
|
239
237
|
<label className="block">
|
|
240
|
-
<span className="mb-1.5 block text-[
|
|
238
|
+
<span className="mb-1.5 block text-[11px] font-medium uppercase tracking-[0.08em] text-zinc-600">
|
|
241
239
|
{label}
|
|
242
|
-
{required ? <span className="text-
|
|
240
|
+
{required ? <span className="text-zinc-500"> *</span> : null}
|
|
243
241
|
</span>
|
|
244
242
|
{children}
|
|
245
243
|
</label>
|
|
@@ -247,12 +245,4 @@ function Field({ label, required, children }: { label: string; required?: boolea
|
|
|
247
245
|
}
|
|
248
246
|
|
|
249
247
|
const inputCls =
|
|
250
|
-
"h-
|
|
251
|
-
|
|
252
|
-
function CheckIcon() {
|
|
253
|
-
return (
|
|
254
|
-
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="3" strokeLinecap="round" strokeLinejoin="round" aria-hidden>
|
|
255
|
-
<path d="M20 6 9 17l-5-5" />
|
|
256
|
-
</svg>
|
|
257
|
-
);
|
|
258
|
-
}
|
|
248
|
+
"h-11 w-full rounded-none border-0 border-b border-zinc-400 bg-transparent px-0 text-[15px] text-ink outline-none transition-colors placeholder:text-zinc-400 focus:border-ink disabled:opacity-60";
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import React from "react";
|
|
2
2
|
import { Link, type PageAuth } from "@pylonsync/react";
|
|
3
3
|
import { siteConfig } from "@/lib/site.config";
|
|
4
|
+
import { WRAP } from "@/components/marketing";
|
|
4
5
|
|
|
5
6
|
// `(marketing)` is a ROUTE GROUP: the parens segment is stripped from every
|
|
6
7
|
// URL (so `(marketing)/page.tsx` still serves `/`), and this layout wraps
|
|
7
|
-
// only the pages inside the group — a
|
|
8
|
-
// both driven by lib/site.config.ts. Routes outside the group (/login,
|
|
8
|
+
// only the pages inside the group — a plain text nav up top and a footer
|
|
9
|
+
// below, both driven by lib/site.config.ts. Routes outside the group (/login,
|
|
9
10
|
// /dashboard) render bare in the root shell. `auth.user_id` is resolved
|
|
10
11
|
// server-side from the session cookie before any HTML is sent, so the nav
|
|
11
12
|
// shows "Dashboard" once the owner is signed in and "Sign in" otherwise —
|
|
@@ -15,6 +16,8 @@ interface LayoutProps {
|
|
|
15
16
|
auth: PageAuth;
|
|
16
17
|
}
|
|
17
18
|
|
|
19
|
+
const NAV_LINK = "text-[14px] text-zinc-700 transition-colors hover:text-ink";
|
|
20
|
+
|
|
18
21
|
export default function MarketingLayout({ children, auth }: LayoutProps) {
|
|
19
22
|
// A guest session (minted by <EnsureGuest> for the live counter) has a
|
|
20
23
|
// `guest_…` user id — that's an anonymous visitor, NOT the signed-in owner,
|
|
@@ -24,43 +27,29 @@ export default function MarketingLayout({ children, auth }: LayoutProps) {
|
|
|
24
27
|
|
|
25
28
|
return (
|
|
26
29
|
<>
|
|
27
|
-
<header className="
|
|
28
|
-
<div className=
|
|
29
|
-
<Link href="/" className="
|
|
30
|
-
|
|
31
|
-
{brand.letter}
|
|
32
|
-
</span>
|
|
33
|
-
<span className="text-[15px] font-semibold tracking-tight text-zinc-900">
|
|
34
|
-
{brand.name}
|
|
35
|
-
</span>
|
|
30
|
+
<header className="bg-white">
|
|
31
|
+
<div className={`${WRAP} flex h-16 items-center justify-between`}>
|
|
32
|
+
<Link href="/" className="font-display text-[1.5rem] leading-none text-ink">
|
|
33
|
+
{brand.name}
|
|
36
34
|
</Link>
|
|
37
|
-
<nav className="flex items-center gap-
|
|
38
|
-
<a href="/work" className=
|
|
35
|
+
<nav className="flex items-center gap-5 sm:gap-7">
|
|
36
|
+
<a href="/work" className={NAV_LINK}>
|
|
39
37
|
Work
|
|
40
38
|
</a>
|
|
41
|
-
<a href="/#services" className=
|
|
39
|
+
<a href="/#services" className={`hidden sm:inline ${NAV_LINK}`}>
|
|
42
40
|
Services
|
|
43
41
|
</a>
|
|
44
42
|
{signedIn ? (
|
|
45
|
-
<Link
|
|
46
|
-
href="/dashboard"
|
|
47
|
-
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"
|
|
48
|
-
>
|
|
43
|
+
<Link href="/dashboard" className={NAV_LINK}>
|
|
49
44
|
Dashboard
|
|
50
45
|
</Link>
|
|
51
46
|
) : (
|
|
52
47
|
<>
|
|
53
|
-
<Link
|
|
54
|
-
href="/login"
|
|
55
|
-
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"
|
|
56
|
-
>
|
|
48
|
+
<Link href="/login" className={`hidden sm:inline ${NAV_LINK}`}>
|
|
57
49
|
Sign in
|
|
58
50
|
</Link>
|
|
59
|
-
<a
|
|
60
|
-
|
|
61
|
-
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"
|
|
62
|
-
>
|
|
63
|
-
{siteConfig.hero.ctaLabel}
|
|
51
|
+
<a href="/#contact" className={NAV_LINK}>
|
|
52
|
+
Contact
|
|
64
53
|
</a>
|
|
65
54
|
</>
|
|
66
55
|
)}
|
|
@@ -78,54 +67,37 @@ export default function MarketingLayout({ children, auth }: LayoutProps) {
|
|
|
78
67
|
function SiteFooter() {
|
|
79
68
|
const { brand } = siteConfig;
|
|
80
69
|
return (
|
|
81
|
-
<footer className="border-t border-zinc-200
|
|
82
|
-
<div className=
|
|
83
|
-
<div className="
|
|
70
|
+
<footer className="border-t border-zinc-200 bg-white text-ink">
|
|
71
|
+
<div className={`${WRAP} py-12`}>
|
|
72
|
+
<div className="grid gap-8 sm:grid-cols-[1fr_auto]">
|
|
84
73
|
<div className="max-w-sm">
|
|
85
|
-
<Link href="/" className="
|
|
86
|
-
|
|
87
|
-
{brand.letter}
|
|
88
|
-
</span>
|
|
89
|
-
<span className="text-[15px] font-semibold tracking-tight text-zinc-900">
|
|
90
|
-
{brand.name}
|
|
91
|
-
</span>
|
|
74
|
+
<Link href="/" className="font-display text-[1.5rem] leading-none">
|
|
75
|
+
{brand.name}
|
|
92
76
|
</Link>
|
|
93
|
-
<p className="mt-3 text-[
|
|
94
|
-
{brand.footerBlurb}
|
|
95
|
-
</p>
|
|
77
|
+
<p className="mt-3 text-[14px] leading-relaxed text-zinc-600">{brand.footerBlurb}</p>
|
|
96
78
|
</div>
|
|
97
|
-
<
|
|
98
|
-
|
|
99
|
-
<a
|
|
100
|
-
|
|
101
|
-
href={s.href}
|
|
102
|
-
aria-label={s.label}
|
|
103
|
-
className="text-zinc-400 transition-colors hover:text-zinc-900"
|
|
104
|
-
>
|
|
105
|
-
<svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor">
|
|
106
|
-
<path d={s.path} />
|
|
107
|
-
</svg>
|
|
79
|
+
<ul className="flex flex-col gap-2 text-[14px] sm:text-right">
|
|
80
|
+
<li>
|
|
81
|
+
<a href={`mailto:${brand.email}`} className="text-zinc-700 transition-colors hover:text-ink">
|
|
82
|
+
{brand.email}
|
|
108
83
|
</a>
|
|
84
|
+
</li>
|
|
85
|
+
{brand.socials.map((s) => (
|
|
86
|
+
<li key={s.label}>
|
|
87
|
+
<a href={s.href} className="text-zinc-700 transition-colors hover:text-ink">
|
|
88
|
+
{s.label}
|
|
89
|
+
</a>
|
|
90
|
+
</li>
|
|
109
91
|
))}
|
|
110
|
-
|
|
111
|
-
href={`mailto:${brand.email}`}
|
|
112
|
-
aria-label="Email"
|
|
113
|
-
className="text-zinc-400 transition-colors hover:text-zinc-900"
|
|
114
|
-
>
|
|
115
|
-
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
|
116
|
-
<rect x="3" y="5" width="18" height="14" rx="2" />
|
|
117
|
-
<path d="m3 7 9 6 9-6" />
|
|
118
|
-
</svg>
|
|
119
|
-
</a>
|
|
120
|
-
</div>
|
|
92
|
+
</ul>
|
|
121
93
|
</div>
|
|
122
|
-
<div className="mt-10 flex flex-col
|
|
94
|
+
<div className="mt-10 flex flex-col gap-2 border-t border-zinc-200 pt-6 text-[13px] text-zinc-500 sm:flex-row sm:justify-between">
|
|
123
95
|
<span>
|
|
124
96
|
© {new Date().getFullYear()} {brand.copyrightName}
|
|
125
97
|
</span>
|
|
126
98
|
<span>
|
|
127
99
|
Built with{" "}
|
|
128
|
-
<a href="https://pylonsync.com" className="
|
|
100
|
+
<a href="https://pylonsync.com" className="text-zinc-700 hover:text-ink">
|
|
129
101
|
Pylon
|
|
130
102
|
</a>
|
|
131
103
|
</span>
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import React from "react";
|
|
2
2
|
import { Link, type NotFoundProps } from "@pylonsync/react";
|
|
3
|
+
import { WRAP_NARROW, TEXT_LINK } from "@/components/marketing";
|
|
3
4
|
|
|
4
5
|
// `(marketing)/not-found.tsx` → rendered at HTTP 404 for any unmatched URL
|
|
5
6
|
// (and when a page calls `response.notFound()`). It lives inside the
|
|
@@ -9,14 +10,11 @@ import { Link, type NotFoundProps } from "@pylonsync/react";
|
|
|
9
10
|
// the link is a client nav.
|
|
10
11
|
export default function NotFound(_props: NotFoundProps) {
|
|
11
12
|
return (
|
|
12
|
-
<div className=
|
|
13
|
-
<h1 className="
|
|
14
|
-
<p className="mt-
|
|
15
|
-
<Link
|
|
16
|
-
|
|
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
|
|
13
|
+
<div className={`${WRAP_NARROW} flex min-h-[60vh] flex-col justify-center py-20 text-ink`}>
|
|
14
|
+
<h1 className="font-display text-[2.75rem] leading-none">Page not found</h1>
|
|
15
|
+
<p className="mt-4 text-[16px] text-zinc-600">There is nothing at this address.</p>
|
|
16
|
+
<Link href="/" className={`mt-6 inline-block self-start text-[15px] ${TEXT_LINK}`}>
|
|
17
|
+
Back to the homepage
|
|
20
18
|
</Link>
|
|
21
19
|
</div>
|
|
22
20
|
);
|