@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
|
@@ -1,15 +1,8 @@
|
|
|
1
1
|
import React from "react";
|
|
2
2
|
import { type Metadata } from "@pylonsync/react";
|
|
3
|
-
import {
|
|
4
|
-
WRAP,
|
|
5
|
-
Eyebrow,
|
|
6
|
-
Divider,
|
|
7
|
-
SectionHead,
|
|
8
|
-
ImagePlaceholder,
|
|
9
|
-
LiveBadge,
|
|
10
|
-
} from "@/components/marketing";
|
|
3
|
+
import { LiveBadge } from "@/components/marketing";
|
|
11
4
|
import { ReservationWidget } from "./reservation-widget";
|
|
12
|
-
import { siteConfig } from "@/lib/site.config";
|
|
5
|
+
import { siteConfig, type DayHours } from "@/lib/site.config";
|
|
13
6
|
|
|
14
7
|
export const metadata: Metadata = {
|
|
15
8
|
title: siteConfig.seo.title,
|
|
@@ -17,75 +10,98 @@ export const metadata: Metadata = {
|
|
|
17
10
|
openGraph: { title: siteConfig.seo.title, description: siteConfig.seo.description, type: "website" },
|
|
18
11
|
};
|
|
19
12
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
//
|
|
24
|
-
//
|
|
13
|
+
const WRAP = "mx-auto w-full max-w-6xl px-6";
|
|
14
|
+
const DAYS = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"];
|
|
15
|
+
|
|
16
|
+
// `app/(marketing)/page.tsx` → `/`. A single dark page: the room behind the
|
|
17
|
+
// hero, opening times, the menu, the live reservation calendar on a cream
|
|
18
|
+
// panel, a review, and where to find us. Every word and every image path
|
|
19
|
+
// comes from lib/site.config.ts. The page never reads `auth`, so the public
|
|
20
|
+
// render stays cacheable; the reservation widget is the one client island.
|
|
25
21
|
export default function LandingPage() {
|
|
26
|
-
const { hero, menu, reservations, reviews, location, faq } = siteConfig;
|
|
22
|
+
const { hero, gallery, menu, reservations, reviews, location, faq } = siteConfig;
|
|
27
23
|
|
|
28
24
|
return (
|
|
29
|
-
<div className="bg-
|
|
30
|
-
{/* HERO */}
|
|
31
|
-
<section className=
|
|
32
|
-
<
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
<QuickFact label="Area" value={hero.quickFacts.area} />
|
|
48
|
-
<QuickFact label="Call" value={hero.quickFacts.phone} />
|
|
49
|
-
</dl>
|
|
50
|
-
</div>
|
|
51
|
-
|
|
52
|
-
{/* Hero photo — replace the placeholder with a real shot of your space. */}
|
|
53
|
-
<div className="relative mx-auto w-full max-w-sm lg:max-w-none">
|
|
54
|
-
<ImagePlaceholder
|
|
55
|
-
shape="portrait"
|
|
56
|
-
title="A photo of your dining room"
|
|
57
|
-
hint="Swap for an <img> in app/(marketing)/page.tsx"
|
|
58
|
-
/>
|
|
59
|
-
<div className="absolute left-4 top-4">
|
|
25
|
+
<div className="bg-[var(--ink)] text-[var(--cream)]">
|
|
26
|
+
{/* HERO: the room, full bleed, the words on top. */}
|
|
27
|
+
<section className="relative isolate min-h-[88vh] overflow-hidden">
|
|
28
|
+
<img src={hero.image} alt="" className="absolute inset-0 -z-20 size-full object-cover" />
|
|
29
|
+
<div className="absolute inset-0 -z-10 bg-gradient-to-t from-[var(--ink)] via-[var(--ink)]/60 to-[var(--ink)]/20" />
|
|
30
|
+
<div className={`${WRAP} flex min-h-[88vh] flex-col justify-end pb-16 pt-32`}>
|
|
31
|
+
<h1 className="font-display mt-5 max-w-3xl text-balance text-[3rem] font-medium leading-[1.02] tracking-[-0.01em] sm:text-[4.5rem]">
|
|
32
|
+
{hero.headline}
|
|
33
|
+
</h1>
|
|
34
|
+
<p className="mt-6 max-w-xl text-[17px] leading-relaxed text-[var(--cream-2)]">{hero.subcopy}</p>
|
|
35
|
+
<div className="mt-8 flex flex-wrap items-center gap-4">
|
|
36
|
+
<a href="#reserve" className="inline-flex items-center rounded-full bg-brand px-6 py-3 text-[14px] font-medium text-white transition-opacity hover:opacity-90">
|
|
37
|
+
{hero.ctaLabel}
|
|
38
|
+
</a>
|
|
39
|
+
<a href="#menu" className="text-[14px] font-medium text-[var(--cream)] underline-offset-4 hover:underline">
|
|
40
|
+
See the menu
|
|
41
|
+
</a>
|
|
42
|
+
<span className="ml-auto hidden sm:block">
|
|
60
43
|
<LiveBadge>Tables update live</LiveBadge>
|
|
61
|
-
</
|
|
44
|
+
</span>
|
|
62
45
|
</div>
|
|
63
46
|
</div>
|
|
64
47
|
</section>
|
|
65
48
|
|
|
49
|
+
{/* OPENING TIMES */}
|
|
50
|
+
<section className="border-y border-white/10">
|
|
51
|
+
<div className={`${WRAP} grid gap-8 py-10 sm:grid-cols-[1fr_2fr] sm:items-start`}>
|
|
52
|
+
<h2 className="font-display text-[26px] italic">Opening times</h2>
|
|
53
|
+
<dl className="grid grid-cols-2 gap-x-8 gap-y-3 text-[14px] sm:grid-cols-3">
|
|
54
|
+
{DAYS.map((day, i) => (
|
|
55
|
+
<div key={day} className="flex justify-between gap-3 border-b border-white/10 pb-2">
|
|
56
|
+
<dt className="text-[var(--cream-2)]">{day}</dt>
|
|
57
|
+
<dd className="font-medium tabular-nums">{hoursLabel(reservations.hours[i])}</dd>
|
|
58
|
+
</div>
|
|
59
|
+
))}
|
|
60
|
+
</dl>
|
|
61
|
+
</div>
|
|
62
|
+
</section>
|
|
63
|
+
|
|
64
|
+
{/* GALLERY */}
|
|
65
|
+
{gallery.length > 0 ? (
|
|
66
|
+
<section className={`${WRAP} grid gap-3 py-12 sm:grid-cols-3`}>
|
|
67
|
+
{gallery.map((g) => (
|
|
68
|
+
<figure key={g.src} className="group">
|
|
69
|
+
<div className="aspect-[4/3] overflow-hidden rounded-[6px] bg-[var(--ink-2)]">
|
|
70
|
+
<img src={g.src} alt={g.caption} className="size-full object-cover transition-transform duration-500 group-hover:scale-[1.03]" loading="lazy" />
|
|
71
|
+
</div>
|
|
72
|
+
<figcaption className="mt-2 font-mono text-[11px] uppercase tracking-[0.16em] text-[var(--cream-2)]">{g.caption}</figcaption>
|
|
73
|
+
</figure>
|
|
74
|
+
))}
|
|
75
|
+
</section>
|
|
76
|
+
) : null}
|
|
77
|
+
|
|
66
78
|
{/* MENU */}
|
|
67
|
-
<
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
79
|
+
<section id="menu" className={`${WRAP} py-16`}>
|
|
80
|
+
<div className="max-w-2xl">
|
|
81
|
+
<h2 className="font-display text-balance text-[2.25rem] leading-[1.08] sm:text-[3rem]">{menu.headline}</h2>
|
|
82
|
+
</div>
|
|
83
|
+
<div className="mt-12 grid gap-12 md:grid-cols-3">
|
|
71
84
|
{menu.sections.map((sec) => (
|
|
72
85
|
<div key={sec.name}>
|
|
73
|
-
<h3 className="font-
|
|
74
|
-
<ul className="mt-
|
|
86
|
+
<h3 className="font-display border-b border-white/15 pb-3 text-[22px] italic">{sec.name}</h3>
|
|
87
|
+
<ul className="mt-5 space-y-5">
|
|
75
88
|
{sec.items.map((it) => (
|
|
76
|
-
<li key={it.name}
|
|
77
|
-
<div>
|
|
78
|
-
<
|
|
79
|
-
|
|
89
|
+
<li key={it.name}>
|
|
90
|
+
<div className="flex items-baseline gap-2">
|
|
91
|
+
<span className="text-[15px] font-medium">{it.name}</span>
|
|
92
|
+
<span className="mb-1 flex-1 border-b border-dotted border-white/25" aria-hidden />
|
|
93
|
+
<span className="text-[14px] tabular-nums text-[var(--cream-2)]">{it.price}</span>
|
|
94
|
+
</div>
|
|
95
|
+
{it.desc || it.tags?.length ? (
|
|
96
|
+
<p className="mt-1 text-[13px] leading-relaxed text-[var(--cream-2)]">
|
|
97
|
+
{it.desc}
|
|
80
98
|
{it.tags?.map((t) => (
|
|
81
|
-
<span key={t} className="
|
|
99
|
+
<span key={t} className="ml-2 font-mono text-[10px] uppercase tracking-wide text-brand">
|
|
82
100
|
{t}
|
|
83
101
|
</span>
|
|
84
102
|
))}
|
|
85
|
-
</
|
|
86
|
-
|
|
87
|
-
</div>
|
|
88
|
-
<span className="shrink-0 text-[14px] font-medium text-zinc-600">{it.price}</span>
|
|
103
|
+
</p>
|
|
104
|
+
) : null}
|
|
89
105
|
</li>
|
|
90
106
|
))}
|
|
91
107
|
</ul>
|
|
@@ -94,66 +110,73 @@ export default function LandingPage() {
|
|
|
94
110
|
</div>
|
|
95
111
|
</section>
|
|
96
112
|
|
|
97
|
-
{/* RESERVE */}
|
|
113
|
+
{/* RESERVE: the live calendar on a cream panel. */}
|
|
98
114
|
{reservations.enabled ? (
|
|
99
|
-
|
|
100
|
-
<
|
|
101
|
-
|
|
102
|
-
|
|
115
|
+
<section id="reserve" className="bg-[var(--paper)] text-zinc-900">
|
|
116
|
+
<div className={`${WRAP} py-16`}>
|
|
117
|
+
<div className="flex flex-col gap-4 sm:flex-row sm:items-end sm:justify-between">
|
|
118
|
+
<div className="max-w-xl">
|
|
119
|
+
<h2 className="font-display text-balance text-[2.25rem] leading-[1.08] sm:text-[3rem]">{reservations.headline}</h2>
|
|
120
|
+
<p className="mt-4 text-[15px] leading-relaxed text-zinc-600">{reservations.subcopy}</p>
|
|
121
|
+
</div>
|
|
122
|
+
<p className="text-[13px] text-zinc-500">
|
|
123
|
+
Parties over {reservations.maxPartySize}? Call{" "}
|
|
124
|
+
<a href={`tel:${location.phone}`} className="font-medium text-zinc-900">
|
|
125
|
+
{location.phone}
|
|
126
|
+
</a>
|
|
127
|
+
</p>
|
|
128
|
+
</div>
|
|
103
129
|
<div className="mt-8">
|
|
104
130
|
<ReservationWidget />
|
|
105
131
|
</div>
|
|
106
|
-
</
|
|
107
|
-
|
|
132
|
+
</div>
|
|
133
|
+
</section>
|
|
108
134
|
) : null}
|
|
109
135
|
|
|
110
|
-
{/* REVIEWS */}
|
|
136
|
+
{/* REVIEWS: one voice at a time. */}
|
|
111
137
|
{reviews && reviews.items.length > 0 ? (
|
|
112
|
-
|
|
113
|
-
<
|
|
114
|
-
<
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
))}
|
|
129
|
-
</div>
|
|
130
|
-
</section>
|
|
131
|
-
</>
|
|
138
|
+
<section className={`${WRAP} py-16`}>
|
|
139
|
+
<h2 className="font-display text-[2rem] leading-[1.1]">{reviews.headline}</h2>
|
|
140
|
+
<div className="mt-6 divide-y divide-white/10">
|
|
141
|
+
{reviews.items.map((r) => (
|
|
142
|
+
<figure key={r.name} className="grid gap-4 py-8 sm:grid-cols-[1fr_auto] sm:items-end">
|
|
143
|
+
<blockquote className="font-display max-w-3xl text-balance text-[1.5rem] leading-[1.3] sm:text-[1.9rem]">
|
|
144
|
+
“{r.quote}”
|
|
145
|
+
</blockquote>
|
|
146
|
+
<figcaption className="text-[13px] text-[var(--cream-2)]">
|
|
147
|
+
{r.name}
|
|
148
|
+
{r.rating ? <span className="ml-2 text-brand">{"★".repeat(r.rating)}</span> : null}
|
|
149
|
+
</figcaption>
|
|
150
|
+
</figure>
|
|
151
|
+
))}
|
|
152
|
+
</div>
|
|
153
|
+
</section>
|
|
132
154
|
) : null}
|
|
133
155
|
|
|
134
|
-
{/*
|
|
135
|
-
<
|
|
136
|
-
|
|
137
|
-
<div className="grid gap-8 lg:grid-cols-[1fr_1fr]">
|
|
156
|
+
{/* VISIT */}
|
|
157
|
+
<section id="visit" className="border-t border-white/10">
|
|
158
|
+
<div className={`${WRAP} grid gap-10 py-16 lg:grid-cols-2`}>
|
|
138
159
|
<div>
|
|
139
|
-
<
|
|
140
|
-
<div className="mt-6 space-y-
|
|
141
|
-
<p>{location.address}</p>
|
|
142
|
-
<p
|
|
160
|
+
<h2 className="font-display text-balance text-[2.25rem] leading-[1.08] sm:text-[3rem]">{location.headline}</h2>
|
|
161
|
+
<div className="mt-6 space-y-2 text-[15px] leading-relaxed text-[var(--cream-2)]">
|
|
162
|
+
<p className="text-[var(--cream)]">{location.address}</p>
|
|
163
|
+
<p>{location.hoursText}</p>
|
|
143
164
|
<p>
|
|
144
|
-
<a href={`tel:${location.phone}`} className="
|
|
145
|
-
|
|
165
|
+
<a href={`tel:${location.phone}`} className="text-[var(--cream)] underline-offset-4 hover:underline">
|
|
166
|
+
{location.phone}
|
|
167
|
+
</a>{" "}
|
|
168
|
+
· <a href={`mailto:${location.email}`} className="underline-offset-4 hover:underline">{location.email}</a>
|
|
146
169
|
</p>
|
|
147
|
-
<a href="#reserve" className="mt-2 inline-flex items-center rounded-full bg-zinc-900 px-5 py-2.5 text-sm font-medium text-white transition-colors hover:bg-zinc-700">
|
|
148
|
-
{siteConfig.hero.ctaLabel}
|
|
149
|
-
</a>
|
|
150
170
|
</div>
|
|
171
|
+
<a href="#reserve" className="mt-8 inline-flex items-center rounded-full border border-white/25 px-5 py-2.5 text-[14px] font-medium transition-colors hover:bg-white/10">
|
|
172
|
+
{hero.ctaLabel}
|
|
173
|
+
</a>
|
|
151
174
|
</div>
|
|
152
175
|
{location.mapEmbedUrl ? (
|
|
153
|
-
<iframe title="Map" src={location.mapEmbedUrl} className="h-
|
|
176
|
+
<iframe title="Map" src={location.mapEmbedUrl} className="h-72 w-full rounded-[6px] border border-white/10 grayscale" loading="lazy" />
|
|
154
177
|
) : (
|
|
155
|
-
<div className="grid h-
|
|
156
|
-
|
|
178
|
+
<div className="grid h-72 place-items-center rounded-[6px] border border-white/10 bg-[var(--ink-2)] text-[13px] text-[var(--cream-2)]">
|
|
179
|
+
Set <code className="mx-1 font-mono">location.mapEmbedUrl</code> to show a map here
|
|
157
180
|
</div>
|
|
158
181
|
)}
|
|
159
182
|
</div>
|
|
@@ -161,34 +184,34 @@ export default function LandingPage() {
|
|
|
161
184
|
|
|
162
185
|
{/* FAQ */}
|
|
163
186
|
{faq && faq.items.length > 0 ? (
|
|
164
|
-
|
|
165
|
-
<
|
|
166
|
-
|
|
167
|
-
<
|
|
168
|
-
<h2 className="mt-4 text-balance text-2xl font-semibold leading-[1.15] tracking-[-0.02em] sm:text-3xl">{faq.headline}</h2>
|
|
169
|
-
<div className="mt-8 divide-y divide-zinc-200/70 border-y border-zinc-200/70">
|
|
187
|
+
<section className="border-t border-white/10">
|
|
188
|
+
<div className={`${WRAP} py-16`}>
|
|
189
|
+
<h2 className="font-display text-[2rem] leading-[1.1]">{faq.headline}</h2>
|
|
190
|
+
<div className="mt-8 max-w-3xl divide-y divide-white/10 border-y border-white/10">
|
|
170
191
|
{faq.items.map((f) => (
|
|
171
192
|
<details key={f.q} className="group py-5">
|
|
172
|
-
<summary className="flex cursor-pointer items-center justify-between text-[
|
|
193
|
+
<summary className="flex cursor-pointer items-center justify-between text-[16px] font-medium marker:hidden [&::-webkit-details-marker]:hidden">
|
|
173
194
|
{f.q}
|
|
174
195
|
<span className="text-brand transition-transform group-open:rotate-45">+</span>
|
|
175
196
|
</summary>
|
|
176
|
-
<p className="mt-3 max-w-2xl text-[
|
|
197
|
+
<p className="mt-3 max-w-2xl text-[14.5px] leading-relaxed text-[var(--cream-2)]">{f.a}</p>
|
|
177
198
|
</details>
|
|
178
199
|
))}
|
|
179
200
|
</div>
|
|
180
|
-
</
|
|
181
|
-
|
|
201
|
+
</div>
|
|
202
|
+
</section>
|
|
182
203
|
) : null}
|
|
183
204
|
</div>
|
|
184
205
|
);
|
|
185
206
|
}
|
|
186
207
|
|
|
187
|
-
function
|
|
188
|
-
return
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
);
|
|
208
|
+
function hoursLabel(h: DayHours): string {
|
|
209
|
+
if (!h) return "Closed";
|
|
210
|
+
return `${clock(h.open)} – ${clock(h.close)}`;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function clock(t: string): string {
|
|
214
|
+
const [hh, mm] = t.split(":").map(Number);
|
|
215
|
+
const h12 = ((hh + 11) % 12) + 1;
|
|
216
|
+
return `${h12}${mm ? `:${String(mm).padStart(2, "0")}` : ""}${hh < 12 ? "am" : "pm"}`;
|
|
194
217
|
}
|
|
@@ -19,6 +19,11 @@
|
|
|
19
19
|
--brand: #4f46e5;
|
|
20
20
|
--brand-soft: #eef2ff;
|
|
21
21
|
--paper: #fafafa;
|
|
22
|
+
/* The marketing pages: a dark room, cream type. Set from lib/site.config.ts. */
|
|
23
|
+
--ink: #151311;
|
|
24
|
+
--ink-2: #1f1b18;
|
|
25
|
+
--cream: #f3ede4;
|
|
26
|
+
--cream-2: #cfc4b4;
|
|
22
27
|
--background: oklch(1 0 0);
|
|
23
28
|
--foreground: oklch(0.141 0.005 285.823);
|
|
24
29
|
--card: oklch(1 0 0);
|
|
@@ -153,3 +158,9 @@
|
|
|
153
158
|
cursor: pointer;
|
|
154
159
|
}
|
|
155
160
|
}
|
|
161
|
+
|
|
162
|
+
/* Headlines and menu section names. Fraunces is declared in app.ts. */
|
|
163
|
+
.font-display {
|
|
164
|
+
font-family: var(--font-display, "Iowan Old Style", "Palatino Linotype", Georgia, serif);
|
|
165
|
+
font-optical-sizing: auto;
|
|
166
|
+
}
|
|
@@ -127,6 +127,16 @@ const manifest = buildManifest({
|
|
|
127
127
|
display: "swap",
|
|
128
128
|
preload: true,
|
|
129
129
|
}),
|
|
130
|
+
// The display face for headlines and menu section names.
|
|
131
|
+
font({
|
|
132
|
+
family: "Fraunces",
|
|
133
|
+
variable: "--font-display",
|
|
134
|
+
weights: ["400", "500", "600"],
|
|
135
|
+
styles: ["normal", "italic"],
|
|
136
|
+
subsets: ["latin"],
|
|
137
|
+
display: "swap",
|
|
138
|
+
preload: true,
|
|
139
|
+
}),
|
|
130
140
|
],
|
|
131
141
|
routes: await discoverAppRoutes(),
|
|
132
142
|
});
|
|
@@ -107,11 +107,14 @@ export function ImagePlaceholder({
|
|
|
107
107
|
shape = "landscape",
|
|
108
108
|
title,
|
|
109
109
|
hint,
|
|
110
|
+
src,
|
|
110
111
|
className = "",
|
|
111
112
|
}: {
|
|
112
113
|
shape?: "landscape" | "portrait" | "square" | "circle";
|
|
113
114
|
title: string;
|
|
114
115
|
hint?: string;
|
|
116
|
+
/** A real image. The template ships one under public/images; swap it for yours. */
|
|
117
|
+
src?: string;
|
|
115
118
|
className?: string;
|
|
116
119
|
}) {
|
|
117
120
|
const aspect =
|
|
@@ -121,6 +124,13 @@ export function ImagePlaceholder({
|
|
|
121
124
|
? "aspect-square"
|
|
122
125
|
: "aspect-[4/3]";
|
|
123
126
|
const radius = shape === "circle" ? "rounded-full" : "rounded-2xl";
|
|
127
|
+
if (src) {
|
|
128
|
+
return (
|
|
129
|
+
<div className={`relative overflow-hidden bg-zinc-100 ${aspect} ${radius} ${className}`}>
|
|
130
|
+
<img src={src} alt={title} className="absolute inset-0 size-full object-cover" loading="lazy" />
|
|
131
|
+
</div>
|
|
132
|
+
);
|
|
133
|
+
}
|
|
124
134
|
return (
|
|
125
135
|
<div
|
|
126
136
|
className={`relative grid place-items-center overflow-hidden border-2 border-dashed border-zinc-300 bg-zinc-50 ${aspect} ${radius} ${className}`}
|
|
@@ -19,7 +19,7 @@ export type BaseConfig = {
|
|
|
19
19
|
copyrightName: string;
|
|
20
20
|
socials: Social[];
|
|
21
21
|
};
|
|
22
|
-
colors: { brand: string; brandSoft: string; paper: string };
|
|
22
|
+
colors: { brand: string; brandSoft: string; paper: string; ink: string };
|
|
23
23
|
seo: { title: string; description: string };
|
|
24
24
|
};
|
|
25
25
|
|
|
@@ -31,16 +31,18 @@ export type Faq = { q: string; a: string };
|
|
|
31
31
|
|
|
32
32
|
export type RestaurantConfig = BaseConfig & {
|
|
33
33
|
hero: {
|
|
34
|
-
tagline: string;
|
|
35
34
|
headline: string;
|
|
36
35
|
subcopy: string;
|
|
37
36
|
ctaLabel: string;
|
|
38
37
|
quickFacts: { hours: string; area: string; phone: string };
|
|
38
|
+
/** Full-bleed photo behind the hero. Ships as public/images/hero.jpg. */
|
|
39
|
+
image: string;
|
|
39
40
|
};
|
|
40
|
-
|
|
41
|
+
/** The photo band under the hero: public/images/*.jpg with a caption each. */
|
|
42
|
+
gallery: Array<{ src: string; caption: string }>;
|
|
43
|
+
menu: { headline: string; sections: MenuSection[] };
|
|
41
44
|
reservations: {
|
|
42
45
|
enabled: boolean;
|
|
43
|
-
eyebrow: string;
|
|
44
46
|
headline: string;
|
|
45
47
|
subcopy: string;
|
|
46
48
|
slotMinutes: number; // gap between seatings, e.g. 30
|
|
@@ -51,9 +53,8 @@ export type RestaurantConfig = BaseConfig & {
|
|
|
51
53
|
hours: Record<number, DayHours>; // 0=Sun … 6=Sat; null = closed
|
|
52
54
|
confirmationMessage: string;
|
|
53
55
|
};
|
|
54
|
-
reviews?: {
|
|
56
|
+
reviews?: { headline: string; items: Review[] };
|
|
55
57
|
location: {
|
|
56
|
-
eyebrow: string;
|
|
57
58
|
headline: string;
|
|
58
59
|
address: string;
|
|
59
60
|
mapEmbedUrl?: string;
|
|
@@ -61,7 +62,7 @@ export type RestaurantConfig = BaseConfig & {
|
|
|
61
62
|
phone: string;
|
|
62
63
|
email: string;
|
|
63
64
|
};
|
|
64
|
-
faq?: {
|
|
65
|
+
faq?: { headline: string; items: Faq[] };
|
|
65
66
|
};
|
|
66
67
|
|
|
67
68
|
/* ----------------------------- config ---------------------------- */
|
|
@@ -84,7 +85,7 @@ export const siteConfig: RestaurantConfig = {
|
|
|
84
85
|
],
|
|
85
86
|
},
|
|
86
87
|
|
|
87
|
-
colors: { brand: "#
|
|
88
|
+
colors: { brand: "#c2571a", brandSoft: "#3a2418", paper: "#f3ede4", ink: "#151311" },
|
|
88
89
|
|
|
89
90
|
seo: {
|
|
90
91
|
title: "Cedar & Vine — a seasonal bistro in Dallas. Reserve a table.",
|
|
@@ -93,7 +94,6 @@ export const siteConfig: RestaurantConfig = {
|
|
|
93
94
|
},
|
|
94
95
|
|
|
95
96
|
hero: {
|
|
96
|
-
tagline: "Dallas · Bishop Arts",
|
|
97
97
|
headline: "A table by the fire, whenever you're ready.",
|
|
98
98
|
subcopy:
|
|
99
99
|
"Seasonal small plates, wood-fired mains, and a short natural-wine list. The reservation calendar shows current table availability.",
|
|
@@ -103,10 +103,16 @@ export const siteConfig: RestaurantConfig = {
|
|
|
103
103
|
area: "Bishop Arts, Dallas",
|
|
104
104
|
phone: "(214) 555-0172",
|
|
105
105
|
},
|
|
106
|
+
image: "/images/hero.jpg",
|
|
106
107
|
},
|
|
107
108
|
|
|
109
|
+
gallery: [
|
|
110
|
+
{ src: "/images/hearth.jpg", caption: "The hearth" },
|
|
111
|
+
{ src: "/images/branzino.jpg", caption: "Wood-fired branzino" },
|
|
112
|
+
{ src: "/images/bar.jpg", caption: "The bar" },
|
|
113
|
+
],
|
|
114
|
+
|
|
108
115
|
menu: {
|
|
109
|
-
eyebrow: "Menu",
|
|
110
116
|
headline: "Short, seasonal, wood-fired.",
|
|
111
117
|
sections: [
|
|
112
118
|
{
|
|
@@ -139,7 +145,6 @@ export const siteConfig: RestaurantConfig = {
|
|
|
139
145
|
|
|
140
146
|
reservations: {
|
|
141
147
|
enabled: true,
|
|
142
|
-
eyebrow: "Reserve",
|
|
143
148
|
headline: "Find a table that's open.",
|
|
144
149
|
subcopy:
|
|
145
150
|
"Pick a date, time, and party size from the live table calendar. We'll hold the reservation for you.",
|
|
@@ -162,7 +167,6 @@ export const siteConfig: RestaurantConfig = {
|
|
|
162
167
|
},
|
|
163
168
|
|
|
164
169
|
reviews: {
|
|
165
|
-
eyebrow: "Reviews",
|
|
166
170
|
headline: "Regulars keep coming back.",
|
|
167
171
|
items: [
|
|
168
172
|
{
|
|
@@ -187,7 +191,6 @@ export const siteConfig: RestaurantConfig = {
|
|
|
187
191
|
},
|
|
188
192
|
|
|
189
193
|
location: {
|
|
190
|
-
eyebrow: "Visit",
|
|
191
194
|
headline: "Find us in Bishop Arts.",
|
|
192
195
|
address: "412 N Bishop Ave, Dallas, TX 75208",
|
|
193
196
|
mapEmbedUrl: "",
|
|
@@ -197,7 +200,6 @@ export const siteConfig: RestaurantConfig = {
|
|
|
197
200
|
},
|
|
198
201
|
|
|
199
202
|
faq: {
|
|
200
|
-
eyebrow: "Questions",
|
|
201
203
|
headline: "Good to know.",
|
|
202
204
|
items: [
|
|
203
205
|
{
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
package/templates/saas/AGENTS.md
CHANGED
|
@@ -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
|
|
package/templates/shop/AGENTS.md
CHANGED
|
@@ -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
|
|
|
Binary file
|
|
Binary file
|
package/templates/todo/AGENTS.md
CHANGED
|
@@ -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
|
|
|
@@ -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
|
|