@pylonsync/create-pylon 0.3.274 → 0.3.275
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 +80 -0
- package/package.json +1 -1
- package/templates/ARCHETYPES.md +339 -0
- package/templates/agency/.env.example +12 -0
- package/templates/agency/AGENTS.md +61 -0
- package/templates/agency/README.md +90 -0
- package/templates/agency/app/auth-form.tsx +129 -0
- package/templates/agency/app/contact-form.tsx +258 -0
- package/templates/agency/app/dashboard/dashboard-client.tsx +286 -0
- package/templates/agency/app/dashboard/page.tsx +70 -0
- package/templates/agency/app/error.tsx +26 -0
- package/templates/agency/app/globals.css +148 -0
- package/templates/agency/app/layout.tsx +174 -0
- package/templates/agency/app/login/page.tsx +39 -0
- package/templates/agency/app/not-found.tsx +19 -0
- package/templates/agency/app/page.tsx +207 -0
- package/templates/agency/app/robots.ts +12 -0
- package/templates/agency/app/sitemap.ts +9 -0
- package/templates/agency/app.ts +135 -0
- package/templates/agency/components/marketing.tsx +148 -0
- package/templates/agency/components/section-scroller.tsx +35 -0
- package/templates/agency/components/ui/button.tsx +56 -0
- package/templates/agency/components/ui/card.tsx +90 -0
- package/templates/agency/components.json +20 -0
- package/templates/agency/functions/bookInquiry.ts +42 -0
- package/templates/agency/functions/declineInquiry.ts +41 -0
- package/templates/agency/functions/inquiriesForOwner.ts +31 -0
- package/templates/agency/functions/seedCapacity.ts +26 -0
- package/templates/agency/functions/setCapacity.ts +32 -0
- package/templates/agency/functions/submitInquiry.ts +55 -0
- package/templates/agency/gitignore +10 -0
- package/templates/agency/lib/agency.ts +27 -0
- package/templates/agency/lib/owner.ts +26 -0
- package/templates/agency/lib/site.config.ts +239 -0
- package/templates/agency/lib/utils.ts +10 -0
- package/templates/agency/package.json +34 -0
- package/templates/agency/tsconfig.json +18 -0
- package/templates/ai-chat/.env.example +33 -0
- package/templates/ai-chat/AGENTS.md +61 -0
- package/templates/ai-chat/README.md +99 -0
- package/templates/ai-chat/app/auth-form.tsx +124 -0
- package/templates/ai-chat/app/chat-client.tsx +414 -0
- package/templates/ai-chat/app/error.tsx +26 -0
- package/templates/ai-chat/app/globals.css +148 -0
- package/templates/ai-chat/app/layout.tsx +75 -0
- package/templates/ai-chat/app/login/page.tsx +39 -0
- package/templates/ai-chat/app/not-found.tsx +19 -0
- package/templates/ai-chat/app/page.tsx +23 -0
- package/templates/ai-chat/app.ts +121 -0
- package/templates/ai-chat/components.json +20 -0
- package/templates/ai-chat/gitignore +10 -0
- package/templates/ai-chat/lib/site.config.ts +103 -0
- package/templates/ai-chat/lib/utils.ts +10 -0
- package/templates/ai-chat/package.json +34 -0
- package/templates/ai-chat/tsconfig.json +18 -0
- package/templates/ai-studio/.env.example +19 -0
- package/templates/ai-studio/AGENTS.md +61 -0
- package/templates/ai-studio/README.md +83 -0
- package/templates/ai-studio/app/auth-form.tsx +124 -0
- package/templates/ai-studio/app/error.tsx +26 -0
- package/templates/ai-studio/app/globals.css +148 -0
- package/templates/ai-studio/app/layout.tsx +75 -0
- package/templates/ai-studio/app/login/page.tsx +39 -0
- package/templates/ai-studio/app/not-found.tsx +19 -0
- package/templates/ai-studio/app/page.tsx +34 -0
- package/templates/ai-studio/app/studio-client.tsx +214 -0
- package/templates/ai-studio/app.ts +108 -0
- package/templates/ai-studio/components.json +20 -0
- package/templates/ai-studio/functions/_getGeneration.ts +25 -0
- package/templates/ai-studio/functions/_updateGeneration.ts +37 -0
- package/templates/ai-studio/functions/generate.ts +42 -0
- package/templates/ai-studio/functions/pollGeneration.ts +134 -0
- package/templates/ai-studio/gitignore +10 -0
- package/templates/ai-studio/lib/site.config.ts +80 -0
- package/templates/ai-studio/lib/studio.ts +52 -0
- package/templates/ai-studio/lib/utils.ts +10 -0
- package/templates/ai-studio/package.json +34 -0
- package/templates/ai-studio/tsconfig.json +18 -0
- package/templates/creator/.env.example +12 -0
- package/templates/creator/AGENTS.md +61 -0
- package/templates/creator/README.md +67 -0
- package/templates/creator/app/auth-form.tsx +129 -0
- package/templates/creator/app/dashboard/dashboard-client.tsx +297 -0
- package/templates/creator/app/dashboard/page.tsx +70 -0
- package/templates/creator/app/error.tsx +26 -0
- package/templates/creator/app/globals.css +148 -0
- package/templates/creator/app/layout.tsx +160 -0
- package/templates/creator/app/login/page.tsx +39 -0
- package/templates/creator/app/newsletter-signup.tsx +162 -0
- package/templates/creator/app/not-found.tsx +19 -0
- package/templates/creator/app/page.tsx +160 -0
- package/templates/creator/app/robots.ts +12 -0
- package/templates/creator/app/sitemap.ts +9 -0
- package/templates/creator/app.ts +134 -0
- package/templates/creator/components/marketing.tsx +148 -0
- package/templates/creator/components/section-scroller.tsx +35 -0
- package/templates/creator/components/ui/button.tsx +56 -0
- package/templates/creator/components/ui/card.tsx +90 -0
- package/templates/creator/components.json +20 -0
- package/templates/creator/functions/subscribe.ts +82 -0
- package/templates/creator/functions/subscriberStats.ts +75 -0
- package/templates/creator/gitignore +10 -0
- package/templates/creator/lib/owner.ts +26 -0
- package/templates/creator/lib/site.config.ts +173 -0
- package/templates/creator/lib/stats.ts +30 -0
- package/templates/creator/lib/utils.ts +10 -0
- package/templates/creator/package.json +34 -0
- package/templates/creator/tsconfig.json +18 -0
- package/templates/default/app/layout.tsx +26 -27
- package/templates/default/app/page.tsx +90 -274
- package/templates/default/lib/products.ts +9 -122
- package/templates/default/lib/site.config.ts +739 -0
- package/templates/default/lib/site.ts +14 -261
- package/templates/directory/.env.example +12 -0
- package/templates/directory/AGENTS.md +61 -0
- package/templates/directory/README.md +80 -0
- package/templates/directory/app/auth-form.tsx +129 -0
- package/templates/directory/app/dashboard/dashboard-client.tsx +205 -0
- package/templates/directory/app/dashboard/page.tsx +70 -0
- package/templates/directory/app/directory-browse.tsx +328 -0
- package/templates/directory/app/error.tsx +26 -0
- package/templates/directory/app/globals.css +148 -0
- package/templates/directory/app/layout.tsx +171 -0
- package/templates/directory/app/login/page.tsx +39 -0
- package/templates/directory/app/not-found.tsx +19 -0
- package/templates/directory/app/page.tsx +50 -0
- package/templates/directory/app/robots.ts +12 -0
- package/templates/directory/app/sitemap.ts +9 -0
- package/templates/directory/app/submit/page.tsx +30 -0
- package/templates/directory/app/submit-form.tsx +151 -0
- package/templates/directory/app.ts +146 -0
- package/templates/directory/components/marketing.tsx +148 -0
- package/templates/directory/components/section-scroller.tsx +35 -0
- package/templates/directory/components/ui/button.tsx +56 -0
- package/templates/directory/components/ui/card.tsx +90 -0
- package/templates/directory/components.json +20 -0
- package/templates/directory/functions/approveSubmission.ts +45 -0
- package/templates/directory/functions/rejectSubmission.ts +20 -0
- package/templates/directory/functions/seedListings.ts +33 -0
- package/templates/directory/functions/submissionsForOwner.ts +29 -0
- package/templates/directory/functions/submitListing.ts +63 -0
- package/templates/directory/functions/upvote.ts +24 -0
- package/templates/directory/gitignore +10 -0
- package/templates/directory/lib/directory.ts +45 -0
- package/templates/directory/lib/owner.ts +26 -0
- package/templates/directory/lib/site.config.ts +130 -0
- package/templates/directory/lib/utils.ts +10 -0
- package/templates/directory/package.json +34 -0
- package/templates/directory/tsconfig.json +18 -0
- package/templates/local-service/.env.example +12 -0
- package/templates/local-service/AGENTS.md +61 -0
- package/templates/local-service/README.md +82 -0
- package/templates/local-service/app/auth-form.tsx +129 -0
- package/templates/local-service/app/booking-widget.tsx +399 -0
- package/templates/local-service/app/dashboard/dashboard-client.tsx +304 -0
- package/templates/local-service/app/dashboard/page.tsx +63 -0
- package/templates/local-service/app/error.tsx +26 -0
- package/templates/local-service/app/globals.css +148 -0
- package/templates/local-service/app/layout.tsx +151 -0
- package/templates/local-service/app/login/page.tsx +39 -0
- package/templates/local-service/app/not-found.tsx +19 -0
- package/templates/local-service/app/page.tsx +233 -0
- package/templates/local-service/app/robots.ts +12 -0
- package/templates/local-service/app/sitemap.ts +9 -0
- package/templates/local-service/app.ts +131 -0
- package/templates/local-service/components/marketing.tsx +162 -0
- package/templates/local-service/components/section-scroller.tsx +35 -0
- package/templates/local-service/components/ui/button.tsx +56 -0
- package/templates/local-service/components/ui/card.tsx +90 -0
- package/templates/local-service/components.json +20 -0
- package/templates/local-service/functions/bookingsForOwner.ts +30 -0
- package/templates/local-service/functions/cancelBooking.ts +27 -0
- package/templates/local-service/functions/confirmBooking.ts +18 -0
- package/templates/local-service/functions/createBooking.ts +98 -0
- package/templates/local-service/gitignore +10 -0
- package/templates/local-service/lib/booking.ts +24 -0
- package/templates/local-service/lib/owner.ts +26 -0
- package/templates/local-service/lib/site.config.ts +232 -0
- package/templates/local-service/lib/slots.ts +97 -0
- package/templates/local-service/lib/utils.ts +10 -0
- package/templates/local-service/package.json +34 -0
- package/templates/local-service/tsconfig.json +18 -0
- package/templates/marketplace/.env.example +9 -0
- package/templates/marketplace/AGENTS.md +61 -0
- package/templates/marketplace/README.md +78 -0
- package/templates/marketplace/app/_components/CategoryIcon.tsx +40 -0
- package/templates/marketplace/app/error.tsx +26 -0
- package/templates/marketplace/app/globals.css +64 -0
- package/templates/marketplace/app/layout.tsx +60 -0
- package/templates/marketplace/app/listing/[id]/page.tsx +163 -0
- package/templates/marketplace/app/me/page.tsx +15 -0
- package/templates/marketplace/app/not-found.tsx +20 -0
- package/templates/marketplace/app/page.tsx +159 -0
- package/templates/marketplace/app/robots.ts +12 -0
- package/templates/marketplace/app/sell/page.tsx +26 -0
- package/templates/marketplace/app/sitemap.ts +14 -0
- package/templates/marketplace/app.ts +190 -0
- package/templates/marketplace/client/AuthNav.tsx +46 -0
- package/templates/marketplace/client/LiveTicker.tsx +104 -0
- package/templates/marketplace/client/LoginCard.tsx +130 -0
- package/templates/marketplace/client/MarketProvider.tsx +148 -0
- package/templates/marketplace/client/MyMarket.tsx +180 -0
- package/templates/marketplace/client/OfferPanel.tsx +355 -0
- package/templates/marketplace/client/SeedOnEmpty.tsx +26 -0
- package/templates/marketplace/client/SellForm.tsx +160 -0
- package/templates/marketplace/client/WatchButton.tsx +88 -0
- package/templates/marketplace/client/market.ts +341 -0
- package/templates/marketplace/functions/buyNow.ts +78 -0
- package/templates/marketplace/functions/makeOffer.ts +65 -0
- package/templates/marketplace/functions/respondToOffer.ts +62 -0
- package/templates/marketplace/functions/seedMarket.ts +90 -0
- package/templates/marketplace/gitignore +10 -0
- package/templates/marketplace/package.json +35 -0
- package/templates/marketplace/tsconfig.json +14 -0
- package/templates/marketplace/ui/badge.tsx +30 -0
- package/templates/marketplace/ui/button.tsx +49 -0
- package/templates/marketplace/ui/card.tsx +48 -0
- package/templates/marketplace/ui/input.tsx +17 -0
- package/templates/marketplace/ui/label.tsx +18 -0
- package/templates/marketplace/ui/textarea.tsx +17 -0
- package/templates/marketplace/ui/tokens.css +32 -0
- package/templates/marketplace/ui/utils.ts +6 -0
- package/templates/restaurant/.env.example +12 -0
- package/templates/restaurant/AGENTS.md +61 -0
- package/templates/restaurant/README.md +77 -0
- package/templates/restaurant/app/auth-form.tsx +129 -0
- package/templates/restaurant/app/dashboard/dashboard-client.tsx +263 -0
- package/templates/restaurant/app/dashboard/page.tsx +59 -0
- package/templates/restaurant/app/error.tsx +26 -0
- package/templates/restaurant/app/globals.css +148 -0
- package/templates/restaurant/app/layout.tsx +151 -0
- package/templates/restaurant/app/login/page.tsx +39 -0
- package/templates/restaurant/app/not-found.tsx +19 -0
- package/templates/restaurant/app/page.tsx +194 -0
- package/templates/restaurant/app/reservation-widget.tsx +359 -0
- package/templates/restaurant/app/robots.ts +12 -0
- package/templates/restaurant/app/sitemap.ts +9 -0
- package/templates/restaurant/app.ts +115 -0
- package/templates/restaurant/components/marketing.tsx +162 -0
- package/templates/restaurant/components/section-scroller.tsx +35 -0
- package/templates/restaurant/components/ui/button.tsx +56 -0
- package/templates/restaurant/components/ui/card.tsx +90 -0
- package/templates/restaurant/components.json +20 -0
- package/templates/restaurant/functions/cancelReservation.ts +26 -0
- package/templates/restaurant/functions/confirmReservation.ts +17 -0
- package/templates/restaurant/functions/createReservation.ts +92 -0
- package/templates/restaurant/functions/reservationsForOwner.ts +28 -0
- package/templates/restaurant/gitignore +10 -0
- package/templates/restaurant/lib/owner.ts +26 -0
- package/templates/restaurant/lib/reservation.ts +22 -0
- package/templates/restaurant/lib/site.config.ts +218 -0
- package/templates/restaurant/lib/slots.ts +55 -0
- package/templates/restaurant/lib/utils.ts +10 -0
- package/templates/restaurant/package.json +34 -0
- package/templates/restaurant/tsconfig.json +18 -0
- package/templates/shop/.env.example +32 -0
- package/templates/shop/AGENTS.md +61 -0
- package/templates/shop/README.md +102 -0
- package/templates/shop/app/auth-form.tsx +129 -0
- package/templates/shop/app/dashboard/dashboard-client.tsx +264 -0
- package/templates/shop/app/dashboard/page.tsx +59 -0
- package/templates/shop/app/error.tsx +26 -0
- package/templates/shop/app/globals.css +148 -0
- package/templates/shop/app/layout.tsx +160 -0
- package/templates/shop/app/login/page.tsx +39 -0
- package/templates/shop/app/not-found.tsx +19 -0
- package/templates/shop/app/page.tsx +95 -0
- package/templates/shop/app/robots.ts +12 -0
- package/templates/shop/app/shop-client.tsx +436 -0
- package/templates/shop/app/sitemap.ts +9 -0
- package/templates/shop/app/success/page.tsx +33 -0
- package/templates/shop/app.ts +134 -0
- package/templates/shop/components/marketing.tsx +96 -0
- package/templates/shop/components/section-scroller.tsx +35 -0
- package/templates/shop/components/ui/button.tsx +56 -0
- package/templates/shop/components/ui/card.tsx +90 -0
- package/templates/shop/components.json +20 -0
- package/templates/shop/functions/cancelOrder.ts +33 -0
- package/templates/shop/functions/checkout.ts +130 -0
- package/templates/shop/functions/fulfillOrder.ts +17 -0
- package/templates/shop/functions/markGroupPaid.ts +26 -0
- package/templates/shop/functions/ordersForOwner.ts +28 -0
- package/templates/shop/functions/releaseGroup.ts +36 -0
- package/templates/shop/functions/reserveCart.ts +87 -0
- package/templates/shop/functions/restockProduct.ts +23 -0
- package/templates/shop/functions/seedProducts.ts +30 -0
- package/templates/shop/functions/stripeWebhook.ts +72 -0
- package/templates/shop/gitignore +10 -0
- package/templates/shop/lib/owner.ts +26 -0
- package/templates/shop/lib/shop.ts +45 -0
- package/templates/shop/lib/site.config.ts +198 -0
- package/templates/shop/lib/utils.ts +10 -0
- package/templates/shop/package.json +35 -0
- package/templates/shop/tsconfig.json +18 -0
- package/templates/waitlist/.env.example +12 -0
- package/templates/waitlist/AGENTS.md +61 -0
- package/templates/waitlist/README.md +81 -0
- package/templates/waitlist/app/auth-form.tsx +129 -0
- package/templates/waitlist/app/dashboard/dashboard-client.tsx +297 -0
- package/templates/waitlist/app/dashboard/page.tsx +70 -0
- package/templates/waitlist/app/error.tsx +26 -0
- package/templates/waitlist/app/globals.css +148 -0
- package/templates/waitlist/app/layout.tsx +158 -0
- package/templates/waitlist/app/login/page.tsx +39 -0
- package/templates/waitlist/app/not-found.tsx +19 -0
- package/templates/waitlist/app/page.tsx +119 -0
- package/templates/waitlist/app/robots.ts +12 -0
- package/templates/waitlist/app/sitemap.ts +9 -0
- package/templates/waitlist/app/waitlist-hero.tsx +219 -0
- package/templates/waitlist/app.ts +134 -0
- package/templates/waitlist/components/marketing.tsx +96 -0
- package/templates/waitlist/components/ui/button.tsx +56 -0
- package/templates/waitlist/components/ui/card.tsx +90 -0
- package/templates/waitlist/components.json +20 -0
- package/templates/waitlist/functions/joinWaitlist.ts +82 -0
- package/templates/waitlist/functions/waitlistStats.ts +75 -0
- package/templates/waitlist/gitignore +10 -0
- package/templates/waitlist/lib/owner.ts +26 -0
- package/templates/waitlist/lib/site.config.ts +178 -0
- package/templates/waitlist/lib/stats.ts +30 -0
- package/templates/waitlist/lib/utils.ts +10 -0
- package/templates/waitlist/package.json +34 -0
- package/templates/waitlist/tsconfig.json +18 -0
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
// Shape of a reservation row as the owner dashboard sees it, shared by the
|
|
2
|
+
// server query (functions/reservationsForOwner.ts) and the client view. The
|
|
3
|
+
// client imports only the type, never server code.
|
|
4
|
+
|
|
5
|
+
export interface ReservationRow {
|
|
6
|
+
id: string;
|
|
7
|
+
startsAt: string;
|
|
8
|
+
partySize: number;
|
|
9
|
+
customerName: string;
|
|
10
|
+
customerEmail: string;
|
|
11
|
+
customerPhone?: string | null;
|
|
12
|
+
notes?: string | null;
|
|
13
|
+
status: string; // "pending" | "confirmed" | "cancelled"
|
|
14
|
+
createdAt: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// reservationsForOwner returns a discriminated result rather than throwing on a
|
|
18
|
+
// non-owner: a query has no `ctx.error`, and a bare throw reaches the client as
|
|
19
|
+
// a stripped HANDLER_ERROR. A non-owner gets `{ authorized: false }` and NO data.
|
|
20
|
+
export type OwnerReservationsResult =
|
|
21
|
+
| { authorized: true; reservations: ReservationRow[] }
|
|
22
|
+
| { authorized: false };
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
// THE single source of truth for everything business-specific. Rebrand the
|
|
2
|
+
// whole site — and reconfigure the reservation engine — by editing this ONE
|
|
3
|
+
// file. The landing page, layout, AND the createReservation server function all
|
|
4
|
+
// read from here, so the menu, seating hours, and table count stay in lockstep.
|
|
5
|
+
//
|
|
6
|
+
// Colors live here (applied as CSS variables on <html> in app/layout.tsx).
|
|
7
|
+
// Fictional demo copy — replace the values, keep the shape.
|
|
8
|
+
|
|
9
|
+
/* ----------------------------- types ----------------------------- */
|
|
10
|
+
|
|
11
|
+
export type Social = { label: string; href: string; path: string };
|
|
12
|
+
|
|
13
|
+
export type BaseConfig = {
|
|
14
|
+
brand: {
|
|
15
|
+
name: string;
|
|
16
|
+
letter: string;
|
|
17
|
+
domain: string;
|
|
18
|
+
email: string;
|
|
19
|
+
footerBlurb: string;
|
|
20
|
+
copyrightName: string;
|
|
21
|
+
socials: Social[];
|
|
22
|
+
};
|
|
23
|
+
colors: { brand: string; brandSoft: string; paper: string };
|
|
24
|
+
seo: { title: string; description: string };
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
export type MenuItem = { name: string; price: string; desc?: string; tags?: string[] };
|
|
28
|
+
export type MenuSection = { name: string; items: MenuItem[] };
|
|
29
|
+
export type DayHours = { open: string; close: string } | null; // last-seating window
|
|
30
|
+
export type Review = { quote: string; name: string; rating?: number };
|
|
31
|
+
export type Faq = { q: string; a: string };
|
|
32
|
+
|
|
33
|
+
export type RestaurantConfig = BaseConfig & {
|
|
34
|
+
hero: {
|
|
35
|
+
tagline: string;
|
|
36
|
+
headline: string;
|
|
37
|
+
subcopy: string;
|
|
38
|
+
ctaLabel: string;
|
|
39
|
+
quickFacts: { hours: string; area: string; phone: string };
|
|
40
|
+
};
|
|
41
|
+
menu: { eyebrow: string; headline: string; sections: MenuSection[] };
|
|
42
|
+
reservations: {
|
|
43
|
+
enabled: boolean;
|
|
44
|
+
eyebrow: string;
|
|
45
|
+
headline: string;
|
|
46
|
+
subcopy: string;
|
|
47
|
+
slotMinutes: number; // gap between seatings, e.g. 30
|
|
48
|
+
leadTimeHours: number;
|
|
49
|
+
daysAhead: number;
|
|
50
|
+
tablesPerSlot: number; // capacity per seating time
|
|
51
|
+
maxPartySize: number;
|
|
52
|
+
hours: Record<number, DayHours>; // 0=Sun … 6=Sat; null = closed
|
|
53
|
+
confirmationMessage: string;
|
|
54
|
+
};
|
|
55
|
+
reviews?: { eyebrow: string; headline: string; items: Review[] };
|
|
56
|
+
location: {
|
|
57
|
+
eyebrow: string;
|
|
58
|
+
headline: string;
|
|
59
|
+
address: string;
|
|
60
|
+
mapEmbedUrl?: string;
|
|
61
|
+
hoursText: string;
|
|
62
|
+
phone: string;
|
|
63
|
+
email: string;
|
|
64
|
+
};
|
|
65
|
+
faq?: { eyebrow: string; headline: string; items: Faq[] };
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
/* ----------------------------- config ---------------------------- */
|
|
69
|
+
|
|
70
|
+
export const siteConfig: RestaurantConfig = {
|
|
71
|
+
brand: {
|
|
72
|
+
name: "Cedar & Vine",
|
|
73
|
+
letter: "C",
|
|
74
|
+
domain: "cedarandvine.com",
|
|
75
|
+
email: "hello@cedarandvine.example",
|
|
76
|
+
footerBlurb:
|
|
77
|
+
"A seasonal neighborhood bistro in Dallas. Wood-fired plates, a short natural-wine list, and a table waiting for you. Reserve in seconds.",
|
|
78
|
+
copyrightName: "Cedar & Vine",
|
|
79
|
+
socials: [
|
|
80
|
+
{
|
|
81
|
+
label: "Instagram",
|
|
82
|
+
href: "https://instagram.com",
|
|
83
|
+
path: "M12 2.2c3.2 0 3.6 0 4.85.07 1.17.05 1.8.25 2.23.41.56.22.96.48 1.38.9.42.42.68.82.9 1.38.16.42.36 1.06.41 2.23.06 1.27.07 1.65.07 4.85s0 3.58-.07 4.85c-.05 1.17-.25 1.8-.41 2.23-.22.56-.48.96-.9 1.38-.42.42-.82.68-1.38.9-.42.16-1.06.36-2.23.41-1.27.06-1.65.07-4.85.07s-3.58 0-4.85-.07c-1.17-.05-1.8-.25-2.23-.41a3.7 3.7 0 0 1-1.38-.9 3.7 3.7 0 0 1-.9-1.38c-.16-.42-.36-1.06-.41-2.23C2.2 15.58 2.2 15.2 2.2 12s0-3.58.07-4.85c.05-1.17.25-1.8.41-2.23.22-.56.48-.96.9-1.38.42-.42.82-.68 1.38-.9.42-.16 1.06-.36 2.23-.41C8.42 2.2 8.8 2.2 12 2.2zm0 1.8c-3.15 0-3.5 0-4.74.07-.9.04-1.38.19-1.7.32-.43.16-.74.36-1.06.68-.32.32-.52.63-.68 1.06-.13.32-.28.8-.32 1.7C3.8 8.5 3.8 8.85 3.8 12s0 3.5.07 4.74c.04.9.19 1.38.32 1.7.16.43.36.74.68 1.06.32.32.63.52 1.06.68.32.13.8.28 1.7.32 1.24.07 1.59.07 4.74.07s3.5 0 4.74-.07c.9-.04 1.38-.19 1.7-.32.43-.16.74-.36 1.06-.68.32-.32.52-.63.68-1.06.13-.32.28-.8.32-1.7.07-1.24.07-1.59.07-4.74s0-3.5-.07-4.74c-.04-.9-.19-1.38-.32-1.7a2.85 2.85 0 0 0-.68-1.06 2.85 2.85 0 0 0-1.06-.68c-.32-.13-.8-.28-1.7-.32C15.5 4 15.15 4 12 4zm0 3.06A4.94 4.94 0 1 0 12 16.94 4.94 4.94 0 0 0 12 7.06zm0 8.15A3.21 3.21 0 1 1 12 8.8a3.21 3.21 0 0 1 0 6.4zm6.3-8.35a1.15 1.15 0 1 1-2.3 0 1.15 1.15 0 0 1 2.3 0z",
|
|
84
|
+
},
|
|
85
|
+
],
|
|
86
|
+
},
|
|
87
|
+
|
|
88
|
+
colors: { brand: "#9f1239", brandSoft: "#ffe4e6", paper: "#fbf7f5" },
|
|
89
|
+
|
|
90
|
+
seo: {
|
|
91
|
+
title: "Cedar & Vine — a seasonal bistro in Dallas. Reserve a table.",
|
|
92
|
+
description:
|
|
93
|
+
"Wood-fired seasonal plates and natural wine in Dallas. See live table availability and reserve in seconds — no phone tag.",
|
|
94
|
+
},
|
|
95
|
+
|
|
96
|
+
hero: {
|
|
97
|
+
tagline: "Dallas · Bishop Arts",
|
|
98
|
+
headline: "A table by the fire, whenever you're ready.",
|
|
99
|
+
subcopy:
|
|
100
|
+
"Seasonal small plates, wood-fired mains, and a short natural-wine list. Reserve a table in seconds — availability is live, so the time you see is a time you can actually get.",
|
|
101
|
+
ctaLabel: "Reserve a table",
|
|
102
|
+
quickFacts: {
|
|
103
|
+
hours: "Wed–Sun, 5–10",
|
|
104
|
+
area: "Bishop Arts, Dallas",
|
|
105
|
+
phone: "(214) 555-0172",
|
|
106
|
+
},
|
|
107
|
+
},
|
|
108
|
+
|
|
109
|
+
menu: {
|
|
110
|
+
eyebrow: "Menu",
|
|
111
|
+
headline: "Short, seasonal, wood-fired.",
|
|
112
|
+
sections: [
|
|
113
|
+
{
|
|
114
|
+
name: "To start",
|
|
115
|
+
items: [
|
|
116
|
+
{ name: "Wood-fired bread", price: "$9", desc: "Cultured butter, sea salt." },
|
|
117
|
+
{ name: "Little gem salad", price: "$14", desc: "Anchovy, lemon, parmesan.", tags: ["GF"] },
|
|
118
|
+
{ name: "Charred carrots", price: "$13", desc: "Whipped feta, dukkah, honey.", tags: ["V"] },
|
|
119
|
+
{ name: "Beef tartare", price: "$18", desc: "Smoked egg yolk, grilled sourdough." },
|
|
120
|
+
],
|
|
121
|
+
},
|
|
122
|
+
{
|
|
123
|
+
name: "Mains",
|
|
124
|
+
items: [
|
|
125
|
+
{ name: "Hearth chicken", price: "$28", desc: "Half bird, schmaltz potatoes, jus." },
|
|
126
|
+
{ name: "Wood-fired branzino", price: "$34", desc: "Fennel, citrus, salsa verde.", tags: ["GF"] },
|
|
127
|
+
{ name: "Rigatoni", price: "$24", desc: "Pork sugo, parmesan, chili.", },
|
|
128
|
+
{ name: "Roasted cauliflower", price: "$22", desc: "Romesco, golden raisin, almond.", tags: ["V", "GF"] },
|
|
129
|
+
],
|
|
130
|
+
},
|
|
131
|
+
{
|
|
132
|
+
name: "Sweets",
|
|
133
|
+
items: [
|
|
134
|
+
{ name: "Olive oil cake", price: "$11", desc: "Mascarpone, citrus." },
|
|
135
|
+
{ name: "Chocolate budino", price: "$12", desc: "Sea salt, crème fraîche." },
|
|
136
|
+
],
|
|
137
|
+
},
|
|
138
|
+
],
|
|
139
|
+
},
|
|
140
|
+
|
|
141
|
+
reservations: {
|
|
142
|
+
enabled: true,
|
|
143
|
+
eyebrow: "Reserve",
|
|
144
|
+
headline: "Find a table that's open.",
|
|
145
|
+
subcopy:
|
|
146
|
+
"Pick a date, a time, and your party size — open tables are live, so what you see is what's actually free. We hold it; you just show up.",
|
|
147
|
+
slotMinutes: 30,
|
|
148
|
+
leadTimeHours: 2,
|
|
149
|
+
daysAhead: 21,
|
|
150
|
+
tablesPerSlot: 6,
|
|
151
|
+
maxPartySize: 8,
|
|
152
|
+
hours: {
|
|
153
|
+
0: { open: "17:00", close: "21:00" }, // Sun
|
|
154
|
+
1: null, // Mon — closed
|
|
155
|
+
2: null, // Tue — closed
|
|
156
|
+
3: { open: "17:00", close: "21:30" },
|
|
157
|
+
4: { open: "17:00", close: "21:30" },
|
|
158
|
+
5: { open: "17:00", close: "22:00" },
|
|
159
|
+
6: { open: "17:00", close: "22:00" },
|
|
160
|
+
},
|
|
161
|
+
confirmationMessage:
|
|
162
|
+
"Your table is reserved. We'll hold it for 15 minutes past your time — see you soon.",
|
|
163
|
+
},
|
|
164
|
+
|
|
165
|
+
reviews: {
|
|
166
|
+
eyebrow: "Reviews",
|
|
167
|
+
headline: "Regulars keep coming back.",
|
|
168
|
+
items: [
|
|
169
|
+
{
|
|
170
|
+
quote:
|
|
171
|
+
"The branzino is perfect and the wine list is exactly the right size. Booking online means we actually get a table on Fridays now.",
|
|
172
|
+
name: "Maya C.",
|
|
173
|
+
rating: 5,
|
|
174
|
+
},
|
|
175
|
+
{
|
|
176
|
+
quote:
|
|
177
|
+
"Our neighborhood spot. Warm room, real cooking, and the reservation page tells you the truth about what's open.",
|
|
178
|
+
name: "Daniel R.",
|
|
179
|
+
rating: 5,
|
|
180
|
+
},
|
|
181
|
+
{
|
|
182
|
+
quote:
|
|
183
|
+
"Took my parents for their anniversary. The team held a quiet corner table — exactly what I asked for in the notes.",
|
|
184
|
+
name: "Hannah K.",
|
|
185
|
+
rating: 5,
|
|
186
|
+
},
|
|
187
|
+
],
|
|
188
|
+
},
|
|
189
|
+
|
|
190
|
+
location: {
|
|
191
|
+
eyebrow: "Visit",
|
|
192
|
+
headline: "Find us in Bishop Arts.",
|
|
193
|
+
address: "412 N Bishop Ave, Dallas, TX 75208",
|
|
194
|
+
mapEmbedUrl: "",
|
|
195
|
+
hoursText: "Wed–Thu 5–9:30 · Fri–Sat 5–10 · Sun 5–9 · Mon–Tue closed",
|
|
196
|
+
phone: "(214) 555-0172",
|
|
197
|
+
email: "hello@cedarandvine.example",
|
|
198
|
+
},
|
|
199
|
+
|
|
200
|
+
faq: {
|
|
201
|
+
eyebrow: "Questions",
|
|
202
|
+
headline: "Good to know.",
|
|
203
|
+
items: [
|
|
204
|
+
{
|
|
205
|
+
q: "How big a party can I book online?",
|
|
206
|
+
a: "Up to eight. For larger groups or private dining, give us a call — we'll take good care of you.",
|
|
207
|
+
},
|
|
208
|
+
{
|
|
209
|
+
q: "What if I'm running late?",
|
|
210
|
+
a: "We hold your table for 15 minutes past your time. Running later than that? Call and we'll do our best.",
|
|
211
|
+
},
|
|
212
|
+
{
|
|
213
|
+
q: "Do you take walk-ins?",
|
|
214
|
+
a: "Always, at the bar when there's room — but reserving guarantees a table and you can see exactly what's open.",
|
|
215
|
+
},
|
|
216
|
+
],
|
|
217
|
+
},
|
|
218
|
+
};
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
// Pure seating-time math — shared by the client picker (which times to show,
|
|
2
|
+
// how many tables are left) and the server createReservation re-check (is this
|
|
3
|
+
// seating still under capacity). Dependency-free + one source of truth so the
|
|
4
|
+
// two sides agree exactly.
|
|
5
|
+
//
|
|
6
|
+
// Reservations sit at fixed seating times (every `slotMinutes` within hours).
|
|
7
|
+
// Capacity is per-seating: each seating has `tablesPerSlot` tables, and a
|
|
8
|
+
// reservation takes one. So availability is a COUNT (how many markers share a
|
|
9
|
+
// startsAt), not an overlap — simpler than duration-based booking.
|
|
10
|
+
//
|
|
11
|
+
// Times are absolute instants (ISO-8601 UTC); seating GENERATION uses local
|
|
12
|
+
// wall-clock hours. Comparing seating instants is timezone-agnostic.
|
|
13
|
+
|
|
14
|
+
/** ISO start instants for every seating on a local calendar day. */
|
|
15
|
+
export function seatingsForDay(opts: {
|
|
16
|
+
dayISODate: string; // local "YYYY-MM-DD"
|
|
17
|
+
open: string; // local "HH:MM"
|
|
18
|
+
close: string; // last seating must be <= close
|
|
19
|
+
slotMinutes: number;
|
|
20
|
+
leadTimeHours: number;
|
|
21
|
+
nowMs: number;
|
|
22
|
+
}): { startsAt: string; past: boolean }[] {
|
|
23
|
+
const { dayISODate, open, close, slotMinutes, leadTimeHours, nowMs } = opts;
|
|
24
|
+
const [y, m, d] = dayISODate.split("-").map(Number);
|
|
25
|
+
const [oh, om] = open.split(":").map(Number);
|
|
26
|
+
const [ch, cm] = close.split(":").map(Number);
|
|
27
|
+
if ([y, m, d, oh, om, ch, cm].some((n) => Number.isNaN(n))) return [];
|
|
28
|
+
|
|
29
|
+
const start = new Date(y, m - 1, d, oh, om, 0, 0).getTime();
|
|
30
|
+
const end = new Date(y, m - 1, d, ch, cm, 0, 0).getTime();
|
|
31
|
+
const minStart = nowMs + leadTimeHours * 3_600_000;
|
|
32
|
+
const step = slotMinutes * 60_000;
|
|
33
|
+
|
|
34
|
+
const out: { startsAt: string; past: boolean }[] = [];
|
|
35
|
+
for (let s = start; s <= end; s += step) {
|
|
36
|
+
out.push({ startsAt: new Date(s).toISOString(), past: s < minStart });
|
|
37
|
+
}
|
|
38
|
+
return out;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** The weekday (0=Sun..6=Sat) for a local "YYYY-MM-DD". */
|
|
42
|
+
export function weekdayOf(dayISODate: string): number {
|
|
43
|
+
const [y, m, d] = dayISODate.split("-").map(Number);
|
|
44
|
+
return new Date(y, m - 1, d).getDay();
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** Local "YYYY-MM-DD" for an instant `daysFromToday` away (0 = today). */
|
|
48
|
+
export function localDateKey(daysFromToday: number, nowMs: number): string {
|
|
49
|
+
const d = new Date(nowMs);
|
|
50
|
+
d.setDate(d.getDate() + daysFromToday);
|
|
51
|
+
const y = d.getFullYear();
|
|
52
|
+
const m = String(d.getMonth() + 1).padStart(2, "0");
|
|
53
|
+
const day = String(d.getDate()).padStart(2, "0");
|
|
54
|
+
return `${y}-${m}-${day}`;
|
|
55
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { type ClassValue, clsx } from "clsx";
|
|
2
|
+
import { twMerge } from "tailwind-merge";
|
|
3
|
+
|
|
4
|
+
// `cn` — the shadcn class merger. clsx resolves conditional/array class
|
|
5
|
+
// inputs; tailwind-merge then dedupes conflicting Tailwind utilities so
|
|
6
|
+
// the last one wins (e.g. `cn("px-2", "px-4")` → "px-4"). Every shadcn
|
|
7
|
+
// component routes its className through this.
|
|
8
|
+
export function cn(...inputs: ClassValue[]) {
|
|
9
|
+
return twMerge(clsx(inputs));
|
|
10
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "__APP_NAME_KEBAB__",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"private": true,
|
|
5
|
+
"type": "module",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"dev": "pylon dev",
|
|
8
|
+
"deploy": "pylon deploy",
|
|
9
|
+
"check": "tsc --noEmit"
|
|
10
|
+
},
|
|
11
|
+
"dependencies": {
|
|
12
|
+
"@pylonsync/react": "^__PYLON_VERSION__",
|
|
13
|
+
"@pylonsync/sdk": "^__PYLON_VERSION__",
|
|
14
|
+
"@pylonsync/functions": "^__PYLON_VERSION__",
|
|
15
|
+
"@pylonsync/client": "^__PYLON_VERSION__",
|
|
16
|
+
"react": "^19.0.0",
|
|
17
|
+
"react-dom": "^19.0.0",
|
|
18
|
+
"tailwindcss": "^4.3.0",
|
|
19
|
+
"@tailwindcss/cli": "^4.3.0",
|
|
20
|
+
"tw-animate-css": "^1.2.0",
|
|
21
|
+
"class-variance-authority": "^0.7.1",
|
|
22
|
+
"clsx": "^2.1.1",
|
|
23
|
+
"tailwind-merge": "^2.5.0",
|
|
24
|
+
"lucide-react": "^0.460.0",
|
|
25
|
+
"@radix-ui/react-slot": "^1.1.0"
|
|
26
|
+
},
|
|
27
|
+
"devDependencies": {
|
|
28
|
+
"@pylonsync/cli": "^__PYLON_VERSION__",
|
|
29
|
+
"@types/node": "^22.0.0",
|
|
30
|
+
"@types/react": "^19.0.0",
|
|
31
|
+
"@types/react-dom": "^19.0.0",
|
|
32
|
+
"typescript": "^5.6.0"
|
|
33
|
+
}
|
|
34
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"module": "ESNext",
|
|
5
|
+
"moduleResolution": "bundler",
|
|
6
|
+
"jsx": "react",
|
|
7
|
+
"esModuleInterop": true,
|
|
8
|
+
"strict": true,
|
|
9
|
+
"skipLibCheck": true,
|
|
10
|
+
"lib": ["ES2022", "DOM"],
|
|
11
|
+
"types": ["react", "react-dom", "node"],
|
|
12
|
+
"baseUrl": ".",
|
|
13
|
+
"paths": {
|
|
14
|
+
"@/*": ["./*"]
|
|
15
|
+
}
|
|
16
|
+
},
|
|
17
|
+
"include": ["app.ts", "app/**/*", "components/**/*", "lib/**/*", "functions/**/*"]
|
|
18
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# Copy to `.env` and fill in. `pylon dev` loads `.env` automatically.
|
|
2
|
+
|
|
3
|
+
# ── Owner (required to use the dashboard) ────────────────────────────────────
|
|
4
|
+
# The /dashboard (orders + stock) is unlocked only for the account whose email
|
|
5
|
+
# matches this value, and the owner-only orders function refuses to return any
|
|
6
|
+
# customer data otherwise. Set this to the email you'll sign in with, then
|
|
7
|
+
# create that account at /login.
|
|
8
|
+
PYLON_OWNER_EMAIL=you@yourshop.com
|
|
9
|
+
|
|
10
|
+
# ── Site URL (optional) ──────────────────────────────────────────────────────
|
|
11
|
+
# Used by robots.txt + sitemap.xml, and to allow-list the Stripe checkout return
|
|
12
|
+
# URL in production. Point it at your real domain. (Pylon Cloud sets
|
|
13
|
+
# PYLON_PUBLIC_URL automatically, which also works.)
|
|
14
|
+
# SITE_URL=https://yourshop.com
|
|
15
|
+
|
|
16
|
+
# ── Stripe checkout (optional) ───────────────────────────────────────────────
|
|
17
|
+
# The store works WITHOUT these — checkout just holds stock and records a
|
|
18
|
+
# "reserved" order for you to follow up on, so you can demo live inventory with
|
|
19
|
+
# zero setup. Add the keys to take real card payments at checkout:
|
|
20
|
+
# 1. STRIPE_SECRET_KEY — from the Stripe dashboard (sk_test_… while testing).
|
|
21
|
+
# With it set, checkout opens a hosted Stripe Checkout page. Prices come
|
|
22
|
+
# from your catalog (lib/site.config.ts) — no Stripe Products to create.
|
|
23
|
+
# 2. Add a webhook endpoint in Stripe →
|
|
24
|
+
# https://<your-app>/api/webhooks/stripeWebhook
|
|
25
|
+
# subscribed to checkout.session.completed / .expired /
|
|
26
|
+
# .async_payment_succeeded / .async_payment_failed, and put its signing
|
|
27
|
+
# secret in STRIPE_WEBHOOK_SECRET. The webhook is what marks an order paid
|
|
28
|
+
# (and returns held stock if a checkout is abandoned).
|
|
29
|
+
# For local testing: `stripe listen --forward-to
|
|
30
|
+
# localhost:3000/api/webhooks/stripeWebhook` prints a whsec_… to use.
|
|
31
|
+
# STRIPE_SECRET_KEY=sk_test_...
|
|
32
|
+
# STRIPE_WEBHOOK_SECRET=whsec_...
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# AGENTS.md — working in a Pylon project
|
|
2
|
+
|
|
3
|
+
Operating rules for a coding agent in this Pylon app. Pylon is a Rails-like framework for realtime apps: you declare entities, policies, and server functions in TypeScript, and a single Rust binary (`pylon`) serves the API, auth, sync, WebSocket, SSE, and native React 19 SSR — one process, one port. The full API reference is at **/llms-full.txt** (served at `/llms-full.txt`; in the repo at `apps/web/public/llms-full.txt`). Read it before guessing an API name.
|
|
4
|
+
|
|
5
|
+
## Directory conventions
|
|
6
|
+
|
|
7
|
+
**Unified SSR app:**
|
|
8
|
+
- `app.ts` — data model + manifest (`entity()` + `field.*`, queries/actions/policies, `routes: await discoverAppRoutes()`). Ends with `console.log(JSON.stringify(manifest))`.
|
|
9
|
+
- `app/` — file-based SSR routes. `app/page.tsx` → `/`, `app/about/page.tsx` → `/about`, `app/blog/[slug]/page.tsx` → `/blog/:slug`. `app/layout.tsx` is the shell; `app/error.tsx` / `app/not-found.tsx` are boundaries.
|
|
10
|
+
- `app/globals.css` — Tailwind v4 entrypoint (auto-compiled and injected).
|
|
11
|
+
- `functions/` — server functions, one per file, `default`-exported.
|
|
12
|
+
- `.pylon/` — local dev state (sqlite, jobs, sessions, uploads). Created by `pylon dev`. Do not commit.
|
|
13
|
+
|
|
14
|
+
**Monorepo app:** backend is `apps/api/` (entry `apps/api/schema.ts`, handlers in `apps/api/functions/`); frontend in `apps/web/`. `pylon.manifest.json` / `pylon.client.ts` are generated — do not hand-edit.
|
|
15
|
+
|
|
16
|
+
## The core authoring loop
|
|
17
|
+
|
|
18
|
+
1. **Define an entity** — `entity("Thing", { name: field.string(), done: field.boolean().default(false) })`. Modifiers: `.optional()`, `.unique()`, `.readonly()` (settable on insert, rejected on client update — use for `authorId`/`orgId`), `.serverOnly()` (never in HTTP responses), `.encrypted()` (AEAD at rest, needs `PYLON_ENCRYPTION_KEY`), `.crdt("text")` (collaborative).
|
|
19
|
+
2. **Write a policy** — `policy({ entity: "Thing", allowRead, allowInsert, allowUpdate, allowDelete })` with CEL-like expressions over `auth.*` / `data.*` (e.g. `"auth.userId == data.authorId"`). **Omitted actions DENY by default.** Wide-open dev policies (`allow*: "true"`) are flagged by `pylon lint` — tighten before shipping.
|
|
20
|
+
3. **Author a function** in `functions/<name>.ts` — `query` (read-only), `mutation` (transactional read+write), or `action` (external I/O, no direct `ctx.db`). Import `{ query, mutation, action, v }` from `@pylonsync/functions`. `auth` defaults to `"user"` (secure-by-default); set `"public"` explicitly for unauthenticated access. Use `ctx.db.*`, `ctx.auth.userId`, `ctx.error(code, msg)`.
|
|
21
|
+
4. **Read it on the client** — `db.useQuery("Thing")` (live, re-renders on any write) or `db.useQueryOne("Thing", id)`. Call functions with `db.fn(name, args)` / `callFn`. On SSR pages, read via `use(serverData.list("Thing"))` inside `<Suspense>`.
|
|
22
|
+
|
|
23
|
+
## Key gotchas
|
|
24
|
+
|
|
25
|
+
- **Policies deny by default; server functions BYPASS them.** Direct client CRUD (`/api/entities/*`) and sync are policy-checked. Functions run with full DB access — enforce trust with `ctx.auth` checks inside the handler, not policies.
|
|
26
|
+
- **Type page props from the SDK, don't hand-roll them.** `import type { PageProps, Metadata } from "@pylonsync/react"`. Every page/layout gets `{ url, params, searchParams, auth, response, serverData }`; `PageProps<{ slug: string }>` types a `[slug]` route's params. Request headers/cookies are intentionally NOT on `PageProps` — they're server-only and stripped from hydration, so reading them in the render would mismatch.
|
|
27
|
+
- **Anonymous output caching is opt-in + earned.** `export const revalidate = 60` (seconds) on a page makes it CDN-cacheable (`public, s-maxage=60`) — but ONLY if the render is auth-INDEPENDENT: it must NOT read `props.auth` (reading it at all opts out, even for anonymous), set no cookie, and the app must not run strict per-caller policies (`PYLON_STRICT_FN_POLICIES`). `export const dynamic = "force-static"` caches until the next deploy; `"force-dynamic"` never caches. Fail-closed: without the opt-in (or if any condition fails) the page is `no-cache`. A page that reads `auth` or sets a cookie is never shared. The SAME earned render is also kept in an **origin disk cache** (`.pylon/.cache/ssr`): a cookie-less GET with no query string is served straight off disk for the TTL — skipping the render entirely — then re-rendered live when stale. The disk cache is namespaced per deploy (wiped on each new build) and OFF in `pylon dev` (so an edit is never masked by a stale entry); invalidation is by the `revalidate` TTL or the next deploy.
|
|
28
|
+
- **No-JS forms use `route.ts` + `<Form>`.** Drop `app/.../route.ts` exporting `export const POST: RouteHandler = async ({ form, db, response, auth }) => { await db.insert("X", {...}); response.redirect("/x?ok=1"); }` (303 POST-redirect-GET by default). Render `<Form action="/x">` (from @pylonsync/react) with plain `<input name=...>` — works with JS off (native POST→handler→redirect) and is enhanced to no-reload when JS is on. The handler's `db` is read+write (mutation trust model — gate on `auth`); CSRF is automatic (Origin gate + SameSite=Lax). Multipart/file uploads aren't supported yet — use urlencoded forms + `/api/files`.
|
|
29
|
+
- **`loading.tsx` streams a skeleton while the page's data resolves.** Drop `app/.../loading.tsx` (default export, page props) and the nearest one becomes a route-level Suspense fallback: Pylon flushes the shell + skeleton immediately, then reveals the real page when its top-level `use(serverData…)` resolves (no blank page). It only shows when the PAGE suspends — a page that wraps its own `<Suspense>` around a child (like `/dashboard` in this template) handles that itself. The skeleton is SERVER-ONLY: don't read `serverData` in it. A page with no `loading.tsx` is buffered (unchanged).
|
|
30
|
+
- **`export const streaming = true` streams a page's OWN inner `<Suspense>` boundaries.** Without it (and without a `loading.tsx`), a page is BUFFERED — the whole document, including suspended children, resolves before the first byte. Opt in and the shell + each inner `<Suspense>` fallback flush immediately, then each boundary's real content streams in as its data resolves (multi-boundary progressive streaming). It's opt-in because it changes the response timing contract: a streaming render commits its HTTP head BEFORE suspended subtrees finish, so (a) it's never CDN/disk cacheable — don't combine with `export const revalidate`; (b) `response.setStatus/setCookie/redirect/notFound` only take effect from the SYNCHRONOUS shell render — a call from inside a suspended subtree is dropped (the runtime logs a loud warning naming what was lost); (c) a `throw` from a deep `<Suspense>` child resolves via its nearest `error.tsx` at HTTP 200, not a 5xx. Hydration is clean for any number of boundaries (the data blob ships before hydration runs). Type the config with `import type { RouteSegmentConfig } from "@pylonsync/react"`.
|
|
31
|
+
- **`error.tsx` / `not-found.tsx` boundaries are HYDRATED (interactive).** `app/.../error.tsx` catches a throw below it (HTTP 500) and receives `{ error: { message, digest }, reset }` (`import type { ErrorBoundaryProps }`) — `reset()` re-attempts the route; the stack NEVER reaches the client (dev overlay + logs only). `app/.../not-found.tsx` renders at 404 (also for `response.notFound()`) and gets the page props (`NotFoundProps`), no `reset`. Both run useState/onClick/hooks.
|
|
32
|
+
- **Client navigation hooks live in @pylonsync/react.** `useRouter()` → `{ push, replace, back, forward, refresh, prefetch }`; `useSearchParams()` → reactive `URLSearchParams`; `usePathname()` → reactive pathname. The hooks are CLIENT-reactive — during SSR they return defaults (empty params / "/"); for server-side URL values read the `url` / `searchParams` page props.
|
|
33
|
+
- **Dynamic + catch-all routes follow Next conventions.** `app/blog/[slug]/page.tsx` → `params.slug`. `app/docs/[...path]/page.tsx` is a catch-all (matches `/docs/a/b/c`; `params.path === "a/b/c"` — `.split("/")` for segments). `app/shop/[[...filters]]/page.tsx` is an optional catch-all (also matches the bare `/shop`, with `params.filters === ""`). A catch-all must be the last segment; static beats dynamic beats catch-all on overlap.
|
|
34
|
+
- **`serverData` (SSR) is READ-ONLY.** No write methods; the runtime rejects write frames (`SSR_WRITE_FORBIDDEN`). Mutations belong in actions/functions, never in a page render.
|
|
35
|
+
- **`response.*` / `response.redirect()` / `response.notFound()` must fire in the synchronous shell render**, before any `await` / `<Suspense>`. The HTTP head commits when the shell is ready — status/headers/cookies set from a suspended subtree are lost, and `redirect`/`notFound` thrown below a Suspense boundary are swallowed.
|
|
36
|
+
- **`ctx.llm` and `ctx.connections` are on mutation + action only, NOT query** (reactive purity). `action` has no direct `ctx.db` — use `ctx.runQuery` / `ctx.runMutation`.
|
|
37
|
+
- **It's `db.useQueryOne`, not `useOne`.** Validators and field types have aliases: `v.bool`/`v.boolean`, `v.float`/`v.number`.
|
|
38
|
+
- **There is no `ctx.files` or `defineWorkflow`/`defineJob`.** Files go through `<FileUpload>` + `/api/files/*`; deferred execution is `ctx.scheduler.runAfter/runAt/cancel`.
|
|
39
|
+
|
|
40
|
+
## Use the CLI — don't guess
|
|
41
|
+
|
|
42
|
+
| Need | Command |
|
|
43
|
+
|---|---|
|
|
44
|
+
| Run the app (SSR + API, hot reload, one port `:4321`) | `pylon dev` (or `npm run dev`) |
|
|
45
|
+
| Regenerate manifest + typed client | `pylon codegen` (Swift client: `pylon codegen client --target swift`) |
|
|
46
|
+
| Validate / diff / push schema | `pylon schema check` \| `diff` \| `push` |
|
|
47
|
+
| Migrations | `pylon migrate create <name>` \| `plan` \| `apply` |
|
|
48
|
+
| Lint policies (PYL001–PYL004) | `pylon lint --strict` |
|
|
49
|
+
| Tests | `pylon test` |
|
|
50
|
+
| Adversarial security probe | `pylon test:security` |
|
|
51
|
+
| Inspect cloud request logs (agent-safe) | `pylon logs --json --limit 50` |
|
|
52
|
+
| Inspect data / entities | `pylon data entities` \| `pylon data list <Entity>` |
|
|
53
|
+
| Call a function | `pylon fn <name> key=value` |
|
|
54
|
+
| Health snapshot | `pylon status` |
|
|
55
|
+
| Build for prod | `pylon build` |
|
|
56
|
+
| Deploy (Pylon Cloud by default) | `pylon deploy` |
|
|
57
|
+
| Look up an error code | `pylon explain <CODE>` |
|
|
58
|
+
|
|
59
|
+
`--json` works on every command for machine-readable output. Prefer one-shot/agent-safe flags (`pylon logs --limit N`, not a blocking `--follow`).
|
|
60
|
+
|
|
61
|
+
For full signatures, env vars, the complete CLI, and SSR/client/server-primitive details: **/llms-full.txt**.
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
# __APP_NAME__
|
|
2
|
+
|
|
3
|
+
A small DTC store built with [Pylon](https://pylonsync.com) — a server-rendered
|
|
4
|
+
storefront with a **cart, real Stripe checkout, and live inventory**, plus a
|
|
5
|
+
private owner dashboard, all from one binary on one port. No Next.js, no
|
|
6
|
+
separate API server.
|
|
7
|
+
|
|
8
|
+
The realtime point: each product shows its stock, and the moment someone buys
|
|
9
|
+
the last unit it flips to "Sold out" for everyone with the page open.
|
|
10
|
+
|
|
11
|
+
## Develop
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
__RUN_DEV__
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
Open http://localhost:4321. Then **open a second tab**, add the last of an item
|
|
18
|
+
to your cart and check out in one, and watch it flip to "Sold out" in the
|
|
19
|
+
other — with no refresh.
|
|
20
|
+
|
|
21
|
+
> Checkout works with **zero config**: with no Stripe keys set, it holds the
|
|
22
|
+
> stock and records a "reserved" order for you to follow up on. Add
|
|
23
|
+
> `STRIPE_SECRET_KEY` (see [Checkout](#checkout)) to take real card payments.
|
|
24
|
+
|
|
25
|
+
## How the realtime works
|
|
26
|
+
|
|
27
|
+
- `Product` is a public-read entity holding live stock; `app/shop-client.tsx`
|
|
28
|
+
reads it with `db.useQuery`, so every card's stock count is live.
|
|
29
|
+
- `functions/checkout.ts` is a public **action**. It calls
|
|
30
|
+
`functions/reserveCart.ts` (an internal **mutation**) to re-check stock under a
|
|
31
|
+
per-product advisory lock and HOLD it before the order is recorded — so two
|
|
32
|
+
shoppers can't both buy the last unit. The stock change syncs to every open
|
|
33
|
+
grid. With Stripe configured it then opens a hosted Checkout Session.
|
|
34
|
+
- `functions/cancelOrder.ts` returns units to stock (a sold-out item can come
|
|
35
|
+
back live); `functions/restockProduct.ts` lets the owner add stock.
|
|
36
|
+
- `functions/seedProducts.ts` loads the catalog from config into the DB on first
|
|
37
|
+
visit (idempotent).
|
|
38
|
+
|
|
39
|
+
## Privacy — read this
|
|
40
|
+
|
|
41
|
+
The `Order` entity holds the customer's name + email (PII), so its policy in
|
|
42
|
+
`app.ts` **denies every client read and write**. The public page only reads
|
|
43
|
+
`Product` (catalog + stock, no PII). Orders come back only through
|
|
44
|
+
`ordersForOwner`, gated to the owner server-side.
|
|
45
|
+
|
|
46
|
+
## The owner dashboard
|
|
47
|
+
|
|
48
|
+
`/dashboard` shows orders (with customer details), fulfill/cancel, and a live
|
|
49
|
+
stock table with one-tap restock — updating live as orders land.
|
|
50
|
+
|
|
51
|
+
Set `PYLON_OWNER_EMAIL` in `.env` (see `.env.example`) to the email you'll sign
|
|
52
|
+
in with, then create that account at `/login`.
|
|
53
|
+
|
|
54
|
+
## Checkout
|
|
55
|
+
|
|
56
|
+
Checkout is a single public `checkout` action that holds stock, then:
|
|
57
|
+
|
|
58
|
+
- **With `STRIPE_SECRET_KEY` set** → opens a hosted **Stripe Checkout** session
|
|
59
|
+
(all cart lines priced inline from your catalog — no Stripe Products to set
|
|
60
|
+
up) and redirects the shopper to it. The signed `stripeWebhook` action (at
|
|
61
|
+
`/api/webhooks/stripeWebhook`) marks the order **paid** on success and
|
|
62
|
+
**returns held stock** if a checkout is abandoned. The webhook signature is
|
|
63
|
+
verified with `@pylonsync/stripe`'s constant-time verifier before any event is
|
|
64
|
+
trusted.
|
|
65
|
+
- **Without Stripe keys** → the order is held as **reserved** for you to follow
|
|
66
|
+
up on. The store still works end-to-end, so you can demo live inventory with
|
|
67
|
+
zero setup.
|
|
68
|
+
|
|
69
|
+
See `.env.example` for the two env vars and the Stripe dashboard / `stripe
|
|
70
|
+
listen` setup.
|
|
71
|
+
|
|
72
|
+
## Rebrand it
|
|
73
|
+
|
|
74
|
+
Everything lives in **`lib/site.config.ts`** — brand, colors, the product list
|
|
75
|
+
(with starting stock), value props, reviews, policies. Edit that one file and
|
|
76
|
+
the whole store re-themes; the products re-seed on a fresh database.
|
|
77
|
+
|
|
78
|
+
## Layout
|
|
79
|
+
|
|
80
|
+
```
|
|
81
|
+
app.ts Product (public, live stock) + Order (PII) + User
|
|
82
|
+
lib/site.config.ts ALL copy + brand + product catalog (edit this)
|
|
83
|
+
functions/seedProducts.ts idempotent catalog seed from config
|
|
84
|
+
functions/checkout.ts public action: hold stock + open Stripe Checkout
|
|
85
|
+
functions/reserveCart.ts internal mutation: race-safe stock hold (per cart)
|
|
86
|
+
functions/stripeWebhook.ts public webhook: verify signature, settle the order
|
|
87
|
+
functions/{markGroupPaid,releaseGroup}.ts internal: settle/restore on webhook
|
|
88
|
+
functions/ordersForOwner.ts owner-only query: orders + customer PII
|
|
89
|
+
functions/{fulfill,cancel}Order.ts, restockProduct.ts owner-only mutations
|
|
90
|
+
app/page.tsx the storefront (server-rendered)
|
|
91
|
+
app/shop-client.tsx client island: live product grid + cart + checkout
|
|
92
|
+
app/success/page.tsx Stripe post-payment landing
|
|
93
|
+
app/dashboard/ owner dashboard (auth-gated, live)
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
## Deploy
|
|
97
|
+
|
|
98
|
+
```bash
|
|
99
|
+
pylon deploy
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
Docs: https://docs.pylonsync.com
|