@pylonsync/create-pylon 0.3.273 → 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,108 @@
|
|
|
1
|
+
import {
|
|
2
|
+
entity,
|
|
3
|
+
field,
|
|
4
|
+
policy,
|
|
5
|
+
auth,
|
|
6
|
+
buildManifest,
|
|
7
|
+
discoverAppRoutes,
|
|
8
|
+
} from "@pylonsync/sdk";
|
|
9
|
+
|
|
10
|
+
// ---------------------------------------------------------------------------
|
|
11
|
+
// ai-studio — a generative media studio (image / audio / video). The realtime
|
|
12
|
+
// hook is the generation gallery: kick off a generation and a "generating…"
|
|
13
|
+
// card appears instantly, then flips to the finished result the moment the
|
|
14
|
+
// server-side `generate` action resolves — live, across every open tab. The
|
|
15
|
+
// provider call (and your API key) stays on the server.
|
|
16
|
+
//
|
|
17
|
+
// Two entities:
|
|
18
|
+
// • Generation — one generation request + its result. Owner-scoped: you only
|
|
19
|
+
// see your own. Written ONLY by the generate action (clients
|
|
20
|
+
// can't insert), and read live via `db.useQuery`.
|
|
21
|
+
// • User — the account (email/password is built in).
|
|
22
|
+
//
|
|
23
|
+
// Multi-user: every signed-in (or guest) visitor gets their own private studio.
|
|
24
|
+
// Image + audio call OpenAI when OPENAI_API_KEY is set; with no key the studio
|
|
25
|
+
// returns a clearly-labeled placeholder so the whole flow + live gallery work
|
|
26
|
+
// with zero config. Video is a stubbed extension point — see functions/generate.ts.
|
|
27
|
+
// ---------------------------------------------------------------------------
|
|
28
|
+
|
|
29
|
+
const Generation = entity(
|
|
30
|
+
"Generation",
|
|
31
|
+
{
|
|
32
|
+
// Stamped from the session inside the generate action (server-side), not by
|
|
33
|
+
// the client — so it's an owner-scoped READ, with no client writes at all.
|
|
34
|
+
userId: field.string(),
|
|
35
|
+
kind: field.string(), // "image" | "audio" | "video"
|
|
36
|
+
prompt: field.string(),
|
|
37
|
+
status: field.string().default("pending"), // "pending" | "processing" | "done" | "failed"
|
|
38
|
+
// The result: an image/audio/video URL (or data: URL), ready to drop into
|
|
39
|
+
// an <img>/<audio>/<video>.
|
|
40
|
+
resultUrl: field.string().optional(),
|
|
41
|
+
error: field.string().optional(),
|
|
42
|
+
// The Replicate prediction id, while a generation is "processing" — the
|
|
43
|
+
// client polls checkGeneration with the row's id until it settles.
|
|
44
|
+
predictionId: field.string().optional(),
|
|
45
|
+
// True when this is the no-API-key placeholder (the UI shows a "demo" badge).
|
|
46
|
+
demo: field.boolean().default(false),
|
|
47
|
+
createdAt: field.datetime().defaultNow(),
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
indexes: [
|
|
51
|
+
{ name: "by_user", fields: ["userId"], unique: false },
|
|
52
|
+
{ name: "by_created", fields: ["createdAt"], unique: false },
|
|
53
|
+
],
|
|
54
|
+
},
|
|
55
|
+
);
|
|
56
|
+
|
|
57
|
+
const User = entity(
|
|
58
|
+
"User",
|
|
59
|
+
{
|
|
60
|
+
email: field.string(),
|
|
61
|
+
displayName: field.string().optional(),
|
|
62
|
+
passwordHash: field.string().serverOnly().optional(),
|
|
63
|
+
avatarColor: field.string().optional(),
|
|
64
|
+
emailVerified: field.datetime().optional(),
|
|
65
|
+
createdAt: field.datetime().defaultNow(),
|
|
66
|
+
},
|
|
67
|
+
{ indexes: [{ name: "by_email", fields: ["email"], unique: true }] },
|
|
68
|
+
);
|
|
69
|
+
|
|
70
|
+
// Generations are PRIVATE per user: you can READ only your own, and you can't
|
|
71
|
+
// write them from the client at all — the generate action (which runs the
|
|
72
|
+
// provider call with the server-side key) is the only writer. So the gallery is
|
|
73
|
+
// live (the sync engine ships you your rows as the action updates them) without
|
|
74
|
+
// ever exposing one user's generations to another.
|
|
75
|
+
const generationPolicy = policy({
|
|
76
|
+
name: "generation_owner_read",
|
|
77
|
+
entity: "Generation",
|
|
78
|
+
allowRead: "auth.userId == data.userId",
|
|
79
|
+
allowInsert: "false",
|
|
80
|
+
allowUpdate: "false",
|
|
81
|
+
allowDelete: "false",
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
const userPolicy = policy({
|
|
85
|
+
name: "user_self",
|
|
86
|
+
entity: "User",
|
|
87
|
+
allowRead: "auth.userId == data.id",
|
|
88
|
+
allowInsert: "false",
|
|
89
|
+
allowUpdate: "false",
|
|
90
|
+
allowDelete: "false",
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
const manifest = buildManifest({
|
|
94
|
+
name: "__APP_NAME__",
|
|
95
|
+
version: "0.1.0",
|
|
96
|
+
entities: [Generation, User],
|
|
97
|
+
// generate (public action) + _createGeneration / _finishGeneration (internal
|
|
98
|
+
// mutations it calls) live in functions/ and are discovered automatically.
|
|
99
|
+
queries: [],
|
|
100
|
+
actions: [],
|
|
101
|
+
policies: [generationPolicy, userPolicy],
|
|
102
|
+
auth: auth(),
|
|
103
|
+
routes: await discoverAppRoutes(),
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
console.log(JSON.stringify(manifest, null, 2));
|
|
107
|
+
|
|
108
|
+
export default manifest;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://ui.shadcn.com/schema.json",
|
|
3
|
+
"style": "new-york",
|
|
4
|
+
"rsc": false,
|
|
5
|
+
"tsx": true,
|
|
6
|
+
"tailwind": {
|
|
7
|
+
"config": "",
|
|
8
|
+
"css": "app/globals.css",
|
|
9
|
+
"baseColor": "zinc",
|
|
10
|
+
"cssVariables": true
|
|
11
|
+
},
|
|
12
|
+
"aliases": {
|
|
13
|
+
"components": "@/components",
|
|
14
|
+
"utils": "@/lib/utils",
|
|
15
|
+
"ui": "@/components/ui",
|
|
16
|
+
"lib": "@/lib",
|
|
17
|
+
"hooks": "@/hooks"
|
|
18
|
+
},
|
|
19
|
+
"iconLibrary": "lucide"
|
|
20
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { query, v } from "@pylonsync/functions";
|
|
2
|
+
|
|
3
|
+
// Internal: read a Generation by id, bypassing the owner read-policy (the
|
|
4
|
+
// pollGeneration job runs without a user session). Returns the fields the job
|
|
5
|
+
// needs, or null.
|
|
6
|
+
export default query<
|
|
7
|
+
{ id: string },
|
|
8
|
+
| {
|
|
9
|
+
id: string;
|
|
10
|
+
kind: string;
|
|
11
|
+
prompt: string;
|
|
12
|
+
status: string;
|
|
13
|
+
predictionId?: string | null;
|
|
14
|
+
}
|
|
15
|
+
| null
|
|
16
|
+
>({
|
|
17
|
+
internal: true,
|
|
18
|
+
args: { id: v.id("Generation") },
|
|
19
|
+
async handler(ctx, args) {
|
|
20
|
+
const g = (await ctx.db.unsafe.get("Generation", args.id)) as
|
|
21
|
+
| { id: string; kind: string; prompt: string; status: string; predictionId?: string | null }
|
|
22
|
+
| null;
|
|
23
|
+
return g;
|
|
24
|
+
},
|
|
25
|
+
});
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { mutation, v } from "@pylonsync/functions";
|
|
2
|
+
|
|
3
|
+
// Internal: patch a Generation. Called by the pollGeneration background job to
|
|
4
|
+
// move a row through pending → processing → done/failed. Each write syncs to the
|
|
5
|
+
// owner's open tabs, so the gallery card updates live. Only set fields are
|
|
6
|
+
// touched. Not client-reachable (Generation is allowInsert/Update:"false").
|
|
7
|
+
export default mutation<
|
|
8
|
+
{
|
|
9
|
+
id: string;
|
|
10
|
+
status?: string;
|
|
11
|
+
resultUrl?: string | null;
|
|
12
|
+
error?: string | null;
|
|
13
|
+
demo?: boolean;
|
|
14
|
+
predictionId?: string;
|
|
15
|
+
},
|
|
16
|
+
{ ok: boolean }
|
|
17
|
+
>({
|
|
18
|
+
internal: true,
|
|
19
|
+
args: {
|
|
20
|
+
id: v.id("Generation"),
|
|
21
|
+
status: v.optional(v.string()),
|
|
22
|
+
resultUrl: v.optional(v.string()),
|
|
23
|
+
error: v.optional(v.string()),
|
|
24
|
+
demo: v.optional(v.boolean()),
|
|
25
|
+
predictionId: v.optional(v.string()),
|
|
26
|
+
},
|
|
27
|
+
async handler(ctx, args) {
|
|
28
|
+
const patch: Record<string, unknown> = {};
|
|
29
|
+
if (args.status !== undefined) patch.status = args.status;
|
|
30
|
+
if (args.resultUrl !== undefined) patch.resultUrl = args.resultUrl;
|
|
31
|
+
if (args.error !== undefined) patch.error = args.error;
|
|
32
|
+
if (args.demo !== undefined) patch.demo = args.demo;
|
|
33
|
+
if (args.predictionId !== undefined) patch.predictionId = args.predictionId;
|
|
34
|
+
await ctx.db.unsafe.update("Generation", args.id, patch);
|
|
35
|
+
return { ok: true };
|
|
36
|
+
},
|
|
37
|
+
});
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { mutation, v } from "@pylonsync/functions";
|
|
2
|
+
|
|
3
|
+
// generate — kick off a generation. A `mutation`: it inserts a pending row
|
|
4
|
+
// (stamped to the caller) and enqueues the `pollGeneration` background job, then
|
|
5
|
+
// returns. It does NO network I/O itself — generation runs in the job, off the
|
|
6
|
+
// request path, so a slow model (video can take minutes) never blocks or times
|
|
7
|
+
// out the call. The pending row syncs to the gallery instantly; the job flips it
|
|
8
|
+
// to the result when it's ready.
|
|
9
|
+
//
|
|
10
|
+
// `auth: "user"` — generating requires a signed-in account (the page already
|
|
11
|
+
// redirects anyone else to /login).
|
|
12
|
+
export default mutation<{ kind: string; prompt: string }, { id: string }>({
|
|
13
|
+
auth: "user",
|
|
14
|
+
args: { kind: v.string(), prompt: v.string() },
|
|
15
|
+
async handler(ctx, args) {
|
|
16
|
+
const userId = ctx.auth.userId;
|
|
17
|
+
if (!userId) throw ctx.error("AUTH_REQUIRED", "Sign in to generate.");
|
|
18
|
+
const kind = args.kind;
|
|
19
|
+
const prompt = args.prompt.trim();
|
|
20
|
+
if (!["image", "audio", "video"].includes(kind)) {
|
|
21
|
+
throw ctx.error("INVALID_ARGS", "kind must be image, audio, or video.");
|
|
22
|
+
}
|
|
23
|
+
if (prompt.length < 2 || prompt.length > 1000) {
|
|
24
|
+
throw ctx.error("INVALID_ARGS", "Enter a prompt (up to 1000 characters).");
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const id = await ctx.db.unsafe.insert("Generation", {
|
|
28
|
+
userId,
|
|
29
|
+
kind,
|
|
30
|
+
prompt,
|
|
31
|
+
status: "pending",
|
|
32
|
+
demo: false,
|
|
33
|
+
createdAt: new Date().toISOString(),
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
// Enqueue the background job (deferred until this mutation commits). It
|
|
37
|
+
// owns the provider call + polling + the final update.
|
|
38
|
+
await ctx.scheduler.runAfter(0, "pollGeneration", { id });
|
|
39
|
+
|
|
40
|
+
return { id };
|
|
41
|
+
},
|
|
42
|
+
});
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import { action, v } from "@pylonsync/functions";
|
|
2
|
+
import { placeholderImage } from "../lib/studio";
|
|
3
|
+
|
|
4
|
+
// pollGeneration — the background job (enqueued by `generate` via
|
|
5
|
+
// ctx.scheduler.runAfter). It runs OFF the request path, so slow models can take
|
|
6
|
+
// as long as they need without blocking or timing out the user's call. It:
|
|
7
|
+
// 1. starts the Replicate prediction (first run), then
|
|
8
|
+
// 2. re-checks it and RESCHEDULES ITSELF every few seconds until it settles,
|
|
9
|
+
// 3. writes the result/error to the Generation — which syncs live to the
|
|
10
|
+
// owner's gallery (pending → processing → done/failed) via db.useQuery.
|
|
11
|
+
//
|
|
12
|
+
// With no REPLICATE_API_TOKEN it returns a clearly-labeled placeholder instead,
|
|
13
|
+
// so the whole flow (and this job) work with zero config.
|
|
14
|
+
//
|
|
15
|
+
// Not internal, but harmless if called directly: it only advances a generation's
|
|
16
|
+
// own prediction; results sync only to the owner (Generation is owner-read).
|
|
17
|
+
|
|
18
|
+
const REPLICATE = "https://api.replicate.com/v1";
|
|
19
|
+
const POLL_MS = 3000;
|
|
20
|
+
const MAX_ATTEMPTS = 200; // ~10 min ceiling, then give up
|
|
21
|
+
|
|
22
|
+
export default action<{ id: string; attempts?: number }, { ok: boolean }>({
|
|
23
|
+
auth: "public",
|
|
24
|
+
args: { id: v.id("Generation"), attempts: v.optional(v.int()) },
|
|
25
|
+
async handler(ctx, args) {
|
|
26
|
+
const attempts = args.attempts ?? 0;
|
|
27
|
+
const gen = await ctx.runQuery<{
|
|
28
|
+
id: string;
|
|
29
|
+
kind: string;
|
|
30
|
+
prompt: string;
|
|
31
|
+
status: string;
|
|
32
|
+
predictionId?: string | null;
|
|
33
|
+
} | null>("_getGeneration", { id: args.id });
|
|
34
|
+
if (!gen || gen.status === "done" || gen.status === "failed") return { ok: true };
|
|
35
|
+
|
|
36
|
+
const update = (patch: Record<string, unknown>) => ctx.runMutation("_updateGeneration", { id: args.id, ...patch });
|
|
37
|
+
const reschedule = () =>
|
|
38
|
+
ctx.scheduler.runAfter(POLL_MS, "pollGeneration", { id: args.id, attempts: attempts + 1 });
|
|
39
|
+
|
|
40
|
+
if (attempts > MAX_ATTEMPTS) {
|
|
41
|
+
await update({ status: "failed", error: "Generation timed out." });
|
|
42
|
+
return { ok: true };
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const token = ctx.env.REPLICATE_API_TOKEN?.trim();
|
|
46
|
+
|
|
47
|
+
// ── Demo mode: no token → a labeled placeholder, no provider call. ──
|
|
48
|
+
if (!token) {
|
|
49
|
+
await update({
|
|
50
|
+
status: "done",
|
|
51
|
+
demo: true,
|
|
52
|
+
resultUrl: gen.kind === "image" ? placeholderImage(gen.prompt) : null,
|
|
53
|
+
});
|
|
54
|
+
return { ok: true };
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
try {
|
|
58
|
+
// First run: kick off the Replicate prediction.
|
|
59
|
+
if (!gen.predictionId) {
|
|
60
|
+
const model = modelFor(gen.kind, ctx.env);
|
|
61
|
+
const pred = await replicateCreate(model, gen.prompt, token);
|
|
62
|
+
if (pred.error) {
|
|
63
|
+
await update({ status: "failed", error: String(pred.error).slice(0, 300) });
|
|
64
|
+
return { ok: true };
|
|
65
|
+
}
|
|
66
|
+
const out = pickOutput(pred.output);
|
|
67
|
+
if (pred.status === "succeeded" && out) {
|
|
68
|
+
await update({ status: "done", resultUrl: out });
|
|
69
|
+
} else if (pred.status === "failed" || pred.status === "canceled") {
|
|
70
|
+
await update({ status: "failed", error: "Provider rejected the request." });
|
|
71
|
+
} else {
|
|
72
|
+
await update({ status: "processing", predictionId: pred.id });
|
|
73
|
+
await reschedule();
|
|
74
|
+
}
|
|
75
|
+
return { ok: true };
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Subsequent runs: check the in-flight prediction.
|
|
79
|
+
const pred = await replicateGet(gen.predictionId, token);
|
|
80
|
+
const out = pickOutput(pred.output);
|
|
81
|
+
if (pred.status === "succeeded" && out) {
|
|
82
|
+
await update({ status: "done", resultUrl: out });
|
|
83
|
+
} else if (pred.status === "failed" || pred.status === "canceled") {
|
|
84
|
+
await update({ status: "failed", error: String(pred.error || "Generation failed.").slice(0, 300) });
|
|
85
|
+
} else {
|
|
86
|
+
await reschedule();
|
|
87
|
+
}
|
|
88
|
+
} catch (err) {
|
|
89
|
+
// Transient network blip → try again; otherwise the attempts cap ends it.
|
|
90
|
+
await reschedule();
|
|
91
|
+
}
|
|
92
|
+
return { ok: true };
|
|
93
|
+
},
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
// Default models (run the latest version via the model-name endpoint). Override
|
|
97
|
+
// any of them with env so swapping models needs no code change.
|
|
98
|
+
function modelFor(kind: string, env: Record<string, string>): string {
|
|
99
|
+
if (kind === "image") return env.REPLICATE_IMAGE_MODEL?.trim() || "black-forest-labs/flux-schnell";
|
|
100
|
+
if (kind === "video") return env.REPLICATE_VIDEO_MODEL?.trim() || "minimax/video-01";
|
|
101
|
+
return env.REPLICATE_AUDIO_MODEL?.trim() || "meta/musicgen";
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function pickOutput(output: unknown): string | null {
|
|
105
|
+
if (Array.isArray(output)) return typeof output[0] === "string" ? output[0] : null;
|
|
106
|
+
return typeof output === "string" ? output : null;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
interface Prediction {
|
|
110
|
+
id: string;
|
|
111
|
+
status: string;
|
|
112
|
+
output?: unknown;
|
|
113
|
+
error?: unknown;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
async function replicateCreate(model: string, prompt: string, token: string): Promise<Prediction> {
|
|
117
|
+
const res = await fetch(`${REPLICATE}/models/${model}/predictions`, {
|
|
118
|
+
method: "POST",
|
|
119
|
+
headers: { Authorization: `Bearer ${token}`, "Content-Type": "application/json" },
|
|
120
|
+
body: JSON.stringify({ input: { prompt } }),
|
|
121
|
+
});
|
|
122
|
+
const j = (await res.json().catch(() => ({}))) as Prediction & { detail?: string };
|
|
123
|
+
if (!res.ok) return { id: "", status: "failed", error: j?.detail || `Replicate ${res.status}` };
|
|
124
|
+
return j;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
async function replicateGet(predictionId: string, token: string): Promise<Prediction> {
|
|
128
|
+
const res = await fetch(`${REPLICATE}/predictions/${predictionId}`, {
|
|
129
|
+
headers: { Authorization: `Bearer ${token}` },
|
|
130
|
+
});
|
|
131
|
+
const j = (await res.json().catch(() => ({}))) as Prediction;
|
|
132
|
+
if (!res.ok) return { id: predictionId, status: "processing" }; // transient — keep polling
|
|
133
|
+
return j;
|
|
134
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
// THE single source of truth for everything brand-specific on this AI media
|
|
2
|
+
// studio. Rebrand by editing this ONE file — the layout + studio UI read from
|
|
3
|
+
// here. The create-pylon scaffolder and Mast target this file.
|
|
4
|
+
//
|
|
5
|
+
// Colors live here (applied as CSS variables on <html> in app/layout.tsx).
|
|
6
|
+
// Fictional demo copy — replace the values, keep the shape.
|
|
7
|
+
|
|
8
|
+
import type { GenerationKind } from "./studio";
|
|
9
|
+
|
|
10
|
+
export type Social = { label: string; href: string; path: string };
|
|
11
|
+
|
|
12
|
+
export type BaseConfig = {
|
|
13
|
+
brand: {
|
|
14
|
+
name: string;
|
|
15
|
+
letter: string;
|
|
16
|
+
domain: string;
|
|
17
|
+
email: string;
|
|
18
|
+
footerBlurb: string;
|
|
19
|
+
copyrightName: string;
|
|
20
|
+
socials: Social[];
|
|
21
|
+
};
|
|
22
|
+
colors: { brand: string; brandSoft: string; paper: string };
|
|
23
|
+
seo: { title: string; description: string };
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
export type StudioConfig = BaseConfig & {
|
|
27
|
+
studio: {
|
|
28
|
+
headline: string;
|
|
29
|
+
subcopy: string;
|
|
30
|
+
inputPlaceholder: string;
|
|
31
|
+
// The generation kinds shown as a selector. `wired` flags which actually
|
|
32
|
+
// call a provider (image + audio); video is a labeled extension point.
|
|
33
|
+
kinds: { id: GenerationKind; label: string; wired: boolean }[];
|
|
34
|
+
// Starter prompts; clicking one fills the box.
|
|
35
|
+
examples: string[];
|
|
36
|
+
};
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
export const siteConfig: StudioConfig = {
|
|
40
|
+
brand: {
|
|
41
|
+
name: "Prism",
|
|
42
|
+
letter: "P",
|
|
43
|
+
domain: "prism.studio",
|
|
44
|
+
email: "hello@prism.example",
|
|
45
|
+
footerBlurb: "A generative media studio built on Pylon — image, audio, and video from a prompt.",
|
|
46
|
+
copyrightName: "Prism",
|
|
47
|
+
socials: [
|
|
48
|
+
{
|
|
49
|
+
label: "X",
|
|
50
|
+
href: "https://x.com",
|
|
51
|
+
path: "M18.244 2.25h3.308l-7.227 8.26 8.502 11.24H16.17l-5.214-6.817L4.99 21.75H1.68l7.73-8.835L1.254 2.25H8.08l4.713 6.231zm-1.161 17.52h1.833L7.084 4.126H5.117z",
|
|
52
|
+
},
|
|
53
|
+
],
|
|
54
|
+
},
|
|
55
|
+
|
|
56
|
+
colors: { brand: "#c026d3", brandSoft: "#fae8ff", paper: "#fdf9fe" },
|
|
57
|
+
|
|
58
|
+
seo: {
|
|
59
|
+
title: "Prism — a generative AI media studio.",
|
|
60
|
+
description:
|
|
61
|
+
"Generate images, audio, and video from a prompt. A live gallery that updates in realtime as each generation finishes, built on Pylon. Your API key stays server-side.",
|
|
62
|
+
},
|
|
63
|
+
|
|
64
|
+
studio: {
|
|
65
|
+
headline: "Make something.",
|
|
66
|
+
subcopy: "Describe it, pick a medium, and watch it land in your gallery — live, the moment it's done.",
|
|
67
|
+
inputPlaceholder: "A neon koi swimming through a rainy Tokyo alley at night…",
|
|
68
|
+
kinds: [
|
|
69
|
+
{ id: "image", label: "Image", wired: true },
|
|
70
|
+
{ id: "audio", label: "Audio", wired: true },
|
|
71
|
+
{ id: "video", label: "Video", wired: true },
|
|
72
|
+
],
|
|
73
|
+
examples: [
|
|
74
|
+
"A cozy reading nook in a treehouse, golden hour, watercolor",
|
|
75
|
+
"An upbeat lo-fi hip-hop loop with mellow piano",
|
|
76
|
+
"Isometric illustration of a tiny island data center",
|
|
77
|
+
"A timelapse of a city skyline as day turns to night",
|
|
78
|
+
],
|
|
79
|
+
},
|
|
80
|
+
};
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
// Shared studio types + the no-API-key placeholder generator (pure, so the
|
|
2
|
+
// generate action can call it server-side and the client imports only the type).
|
|
3
|
+
|
|
4
|
+
export type GenerationKind = "image" | "audio" | "video";
|
|
5
|
+
|
|
6
|
+
export interface GenerationRow {
|
|
7
|
+
id: string;
|
|
8
|
+
userId: string;
|
|
9
|
+
kind: string;
|
|
10
|
+
prompt: string;
|
|
11
|
+
status: string; // "pending" | "done" | "failed"
|
|
12
|
+
resultUrl?: string | null;
|
|
13
|
+
error?: string | null;
|
|
14
|
+
demo: boolean;
|
|
15
|
+
createdAt: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// Deterministic small hash → drives the placeholder gradient hue.
|
|
19
|
+
function hashHue(s: string): number {
|
|
20
|
+
let h = 0;
|
|
21
|
+
for (let i = 0; i < s.length; i++) h = (h * 31 + s.charCodeAt(i)) >>> 0;
|
|
22
|
+
return h % 360;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function escapeXml(s: string): string {
|
|
26
|
+
return s
|
|
27
|
+
.replace(/&/g, "&")
|
|
28
|
+
.replace(/</g, "<")
|
|
29
|
+
.replace(/>/g, ">")
|
|
30
|
+
.replace(/"/g, """)
|
|
31
|
+
.replace(/'/g, "'");
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// A self-contained SVG "image" data URL — used when no OPENAI_API_KEY is set, so
|
|
35
|
+
// the gallery + realtime flow work with zero config. Clearly a placeholder (it
|
|
36
|
+
// renders the prompt over a gradient), not a fake photo. Returns a `data:` URL
|
|
37
|
+
// that drops straight into an <img>.
|
|
38
|
+
export function placeholderImage(prompt: string): string {
|
|
39
|
+
const hue = hashHue(prompt);
|
|
40
|
+
const hue2 = (hue + 60) % 360;
|
|
41
|
+
const text = escapeXml(prompt.slice(0, 80));
|
|
42
|
+
const svg = `<svg xmlns="http://www.w3.org/2000/svg" width="640" height="640" viewBox="0 0 640 640">
|
|
43
|
+
<defs><linearGradient id="g" x1="0" y1="0" x2="1" y2="1">
|
|
44
|
+
<stop offset="0" stop-color="hsl(${hue} 70% 55%)"/>
|
|
45
|
+
<stop offset="1" stop-color="hsl(${hue2} 70% 45%)"/>
|
|
46
|
+
</linearGradient></defs>
|
|
47
|
+
<rect width="640" height="640" fill="url(#g)"/>
|
|
48
|
+
<text x="50%" y="44%" fill="rgba(255,255,255,0.95)" font-family="system-ui,sans-serif" font-size="26" font-weight="600" text-anchor="middle">${text}</text>
|
|
49
|
+
<text x="50%" y="54%" fill="rgba(255,255,255,0.7)" font-family="system-ui,sans-serif" font-size="15" text-anchor="middle">placeholder · set OPENAI_API_KEY for real images</text>
|
|
50
|
+
</svg>`;
|
|
51
|
+
return `data:image/svg+xml;utf8,${encodeURIComponent(svg)}`;
|
|
52
|
+
}
|
|
@@ -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,12 @@
|
|
|
1
|
+
# Copy to `.env` and fill in. `pylon dev` loads `.env` automatically.
|
|
2
|
+
|
|
3
|
+
# ── Owner (required to use the dashboard) ────────────────────────────────────
|
|
4
|
+
# A waitlist is single-tenant: one business, one owner. The /dashboard is
|
|
5
|
+
# unlocked only for the account whose email matches this value, and the
|
|
6
|
+
# owner-only data function refuses to return any signups otherwise. Set this to
|
|
7
|
+
# the email you'll sign in with, then create that account at /login.
|
|
8
|
+
PYLON_OWNER_EMAIL=you@yourbusiness.com
|
|
9
|
+
|
|
10
|
+
# ── Site URL (optional) ──────────────────────────────────────────────────────
|
|
11
|
+
# Used by robots.txt + sitemap.xml. Point it at your real domain in production.
|
|
12
|
+
# SITE_URL=https://yourbusiness.com
|