@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.
Files changed (323) hide show
  1. package/bin/create-pylon.js +80 -0
  2. package/package.json +1 -1
  3. package/templates/ARCHETYPES.md +339 -0
  4. package/templates/agency/.env.example +12 -0
  5. package/templates/agency/AGENTS.md +61 -0
  6. package/templates/agency/README.md +90 -0
  7. package/templates/agency/app/auth-form.tsx +129 -0
  8. package/templates/agency/app/contact-form.tsx +258 -0
  9. package/templates/agency/app/dashboard/dashboard-client.tsx +286 -0
  10. package/templates/agency/app/dashboard/page.tsx +70 -0
  11. package/templates/agency/app/error.tsx +26 -0
  12. package/templates/agency/app/globals.css +148 -0
  13. package/templates/agency/app/layout.tsx +174 -0
  14. package/templates/agency/app/login/page.tsx +39 -0
  15. package/templates/agency/app/not-found.tsx +19 -0
  16. package/templates/agency/app/page.tsx +207 -0
  17. package/templates/agency/app/robots.ts +12 -0
  18. package/templates/agency/app/sitemap.ts +9 -0
  19. package/templates/agency/app.ts +135 -0
  20. package/templates/agency/components/marketing.tsx +148 -0
  21. package/templates/agency/components/section-scroller.tsx +35 -0
  22. package/templates/agency/components/ui/button.tsx +56 -0
  23. package/templates/agency/components/ui/card.tsx +90 -0
  24. package/templates/agency/components.json +20 -0
  25. package/templates/agency/functions/bookInquiry.ts +42 -0
  26. package/templates/agency/functions/declineInquiry.ts +41 -0
  27. package/templates/agency/functions/inquiriesForOwner.ts +31 -0
  28. package/templates/agency/functions/seedCapacity.ts +26 -0
  29. package/templates/agency/functions/setCapacity.ts +32 -0
  30. package/templates/agency/functions/submitInquiry.ts +55 -0
  31. package/templates/agency/gitignore +10 -0
  32. package/templates/agency/lib/agency.ts +27 -0
  33. package/templates/agency/lib/owner.ts +26 -0
  34. package/templates/agency/lib/site.config.ts +239 -0
  35. package/templates/agency/lib/utils.ts +10 -0
  36. package/templates/agency/package.json +34 -0
  37. package/templates/agency/tsconfig.json +18 -0
  38. package/templates/ai-chat/.env.example +33 -0
  39. package/templates/ai-chat/AGENTS.md +61 -0
  40. package/templates/ai-chat/README.md +99 -0
  41. package/templates/ai-chat/app/auth-form.tsx +124 -0
  42. package/templates/ai-chat/app/chat-client.tsx +414 -0
  43. package/templates/ai-chat/app/error.tsx +26 -0
  44. package/templates/ai-chat/app/globals.css +148 -0
  45. package/templates/ai-chat/app/layout.tsx +75 -0
  46. package/templates/ai-chat/app/login/page.tsx +39 -0
  47. package/templates/ai-chat/app/not-found.tsx +19 -0
  48. package/templates/ai-chat/app/page.tsx +23 -0
  49. package/templates/ai-chat/app.ts +121 -0
  50. package/templates/ai-chat/components.json +20 -0
  51. package/templates/ai-chat/gitignore +10 -0
  52. package/templates/ai-chat/lib/site.config.ts +103 -0
  53. package/templates/ai-chat/lib/utils.ts +10 -0
  54. package/templates/ai-chat/package.json +34 -0
  55. package/templates/ai-chat/tsconfig.json +18 -0
  56. package/templates/ai-studio/.env.example +19 -0
  57. package/templates/ai-studio/AGENTS.md +61 -0
  58. package/templates/ai-studio/README.md +83 -0
  59. package/templates/ai-studio/app/auth-form.tsx +124 -0
  60. package/templates/ai-studio/app/error.tsx +26 -0
  61. package/templates/ai-studio/app/globals.css +148 -0
  62. package/templates/ai-studio/app/layout.tsx +75 -0
  63. package/templates/ai-studio/app/login/page.tsx +39 -0
  64. package/templates/ai-studio/app/not-found.tsx +19 -0
  65. package/templates/ai-studio/app/page.tsx +34 -0
  66. package/templates/ai-studio/app/studio-client.tsx +214 -0
  67. package/templates/ai-studio/app.ts +108 -0
  68. package/templates/ai-studio/components.json +20 -0
  69. package/templates/ai-studio/functions/_getGeneration.ts +25 -0
  70. package/templates/ai-studio/functions/_updateGeneration.ts +37 -0
  71. package/templates/ai-studio/functions/generate.ts +42 -0
  72. package/templates/ai-studio/functions/pollGeneration.ts +134 -0
  73. package/templates/ai-studio/gitignore +10 -0
  74. package/templates/ai-studio/lib/site.config.ts +80 -0
  75. package/templates/ai-studio/lib/studio.ts +52 -0
  76. package/templates/ai-studio/lib/utils.ts +10 -0
  77. package/templates/ai-studio/package.json +34 -0
  78. package/templates/ai-studio/tsconfig.json +18 -0
  79. package/templates/creator/.env.example +12 -0
  80. package/templates/creator/AGENTS.md +61 -0
  81. package/templates/creator/README.md +67 -0
  82. package/templates/creator/app/auth-form.tsx +129 -0
  83. package/templates/creator/app/dashboard/dashboard-client.tsx +297 -0
  84. package/templates/creator/app/dashboard/page.tsx +70 -0
  85. package/templates/creator/app/error.tsx +26 -0
  86. package/templates/creator/app/globals.css +148 -0
  87. package/templates/creator/app/layout.tsx +160 -0
  88. package/templates/creator/app/login/page.tsx +39 -0
  89. package/templates/creator/app/newsletter-signup.tsx +162 -0
  90. package/templates/creator/app/not-found.tsx +19 -0
  91. package/templates/creator/app/page.tsx +160 -0
  92. package/templates/creator/app/robots.ts +12 -0
  93. package/templates/creator/app/sitemap.ts +9 -0
  94. package/templates/creator/app.ts +134 -0
  95. package/templates/creator/components/marketing.tsx +148 -0
  96. package/templates/creator/components/section-scroller.tsx +35 -0
  97. package/templates/creator/components/ui/button.tsx +56 -0
  98. package/templates/creator/components/ui/card.tsx +90 -0
  99. package/templates/creator/components.json +20 -0
  100. package/templates/creator/functions/subscribe.ts +82 -0
  101. package/templates/creator/functions/subscriberStats.ts +75 -0
  102. package/templates/creator/gitignore +10 -0
  103. package/templates/creator/lib/owner.ts +26 -0
  104. package/templates/creator/lib/site.config.ts +173 -0
  105. package/templates/creator/lib/stats.ts +30 -0
  106. package/templates/creator/lib/utils.ts +10 -0
  107. package/templates/creator/package.json +34 -0
  108. package/templates/creator/tsconfig.json +18 -0
  109. package/templates/default/app/layout.tsx +26 -27
  110. package/templates/default/app/page.tsx +90 -274
  111. package/templates/default/lib/products.ts +9 -122
  112. package/templates/default/lib/site.config.ts +739 -0
  113. package/templates/default/lib/site.ts +14 -261
  114. package/templates/directory/.env.example +12 -0
  115. package/templates/directory/AGENTS.md +61 -0
  116. package/templates/directory/README.md +80 -0
  117. package/templates/directory/app/auth-form.tsx +129 -0
  118. package/templates/directory/app/dashboard/dashboard-client.tsx +205 -0
  119. package/templates/directory/app/dashboard/page.tsx +70 -0
  120. package/templates/directory/app/directory-browse.tsx +328 -0
  121. package/templates/directory/app/error.tsx +26 -0
  122. package/templates/directory/app/globals.css +148 -0
  123. package/templates/directory/app/layout.tsx +171 -0
  124. package/templates/directory/app/login/page.tsx +39 -0
  125. package/templates/directory/app/not-found.tsx +19 -0
  126. package/templates/directory/app/page.tsx +50 -0
  127. package/templates/directory/app/robots.ts +12 -0
  128. package/templates/directory/app/sitemap.ts +9 -0
  129. package/templates/directory/app/submit/page.tsx +30 -0
  130. package/templates/directory/app/submit-form.tsx +151 -0
  131. package/templates/directory/app.ts +146 -0
  132. package/templates/directory/components/marketing.tsx +148 -0
  133. package/templates/directory/components/section-scroller.tsx +35 -0
  134. package/templates/directory/components/ui/button.tsx +56 -0
  135. package/templates/directory/components/ui/card.tsx +90 -0
  136. package/templates/directory/components.json +20 -0
  137. package/templates/directory/functions/approveSubmission.ts +45 -0
  138. package/templates/directory/functions/rejectSubmission.ts +20 -0
  139. package/templates/directory/functions/seedListings.ts +33 -0
  140. package/templates/directory/functions/submissionsForOwner.ts +29 -0
  141. package/templates/directory/functions/submitListing.ts +63 -0
  142. package/templates/directory/functions/upvote.ts +24 -0
  143. package/templates/directory/gitignore +10 -0
  144. package/templates/directory/lib/directory.ts +45 -0
  145. package/templates/directory/lib/owner.ts +26 -0
  146. package/templates/directory/lib/site.config.ts +130 -0
  147. package/templates/directory/lib/utils.ts +10 -0
  148. package/templates/directory/package.json +34 -0
  149. package/templates/directory/tsconfig.json +18 -0
  150. package/templates/local-service/.env.example +12 -0
  151. package/templates/local-service/AGENTS.md +61 -0
  152. package/templates/local-service/README.md +82 -0
  153. package/templates/local-service/app/auth-form.tsx +129 -0
  154. package/templates/local-service/app/booking-widget.tsx +399 -0
  155. package/templates/local-service/app/dashboard/dashboard-client.tsx +304 -0
  156. package/templates/local-service/app/dashboard/page.tsx +63 -0
  157. package/templates/local-service/app/error.tsx +26 -0
  158. package/templates/local-service/app/globals.css +148 -0
  159. package/templates/local-service/app/layout.tsx +151 -0
  160. package/templates/local-service/app/login/page.tsx +39 -0
  161. package/templates/local-service/app/not-found.tsx +19 -0
  162. package/templates/local-service/app/page.tsx +233 -0
  163. package/templates/local-service/app/robots.ts +12 -0
  164. package/templates/local-service/app/sitemap.ts +9 -0
  165. package/templates/local-service/app.ts +131 -0
  166. package/templates/local-service/components/marketing.tsx +162 -0
  167. package/templates/local-service/components/section-scroller.tsx +35 -0
  168. package/templates/local-service/components/ui/button.tsx +56 -0
  169. package/templates/local-service/components/ui/card.tsx +90 -0
  170. package/templates/local-service/components.json +20 -0
  171. package/templates/local-service/functions/bookingsForOwner.ts +30 -0
  172. package/templates/local-service/functions/cancelBooking.ts +27 -0
  173. package/templates/local-service/functions/confirmBooking.ts +18 -0
  174. package/templates/local-service/functions/createBooking.ts +98 -0
  175. package/templates/local-service/gitignore +10 -0
  176. package/templates/local-service/lib/booking.ts +24 -0
  177. package/templates/local-service/lib/owner.ts +26 -0
  178. package/templates/local-service/lib/site.config.ts +232 -0
  179. package/templates/local-service/lib/slots.ts +97 -0
  180. package/templates/local-service/lib/utils.ts +10 -0
  181. package/templates/local-service/package.json +34 -0
  182. package/templates/local-service/tsconfig.json +18 -0
  183. package/templates/marketplace/.env.example +9 -0
  184. package/templates/marketplace/AGENTS.md +61 -0
  185. package/templates/marketplace/README.md +78 -0
  186. package/templates/marketplace/app/_components/CategoryIcon.tsx +40 -0
  187. package/templates/marketplace/app/error.tsx +26 -0
  188. package/templates/marketplace/app/globals.css +64 -0
  189. package/templates/marketplace/app/layout.tsx +60 -0
  190. package/templates/marketplace/app/listing/[id]/page.tsx +163 -0
  191. package/templates/marketplace/app/me/page.tsx +15 -0
  192. package/templates/marketplace/app/not-found.tsx +20 -0
  193. package/templates/marketplace/app/page.tsx +159 -0
  194. package/templates/marketplace/app/robots.ts +12 -0
  195. package/templates/marketplace/app/sell/page.tsx +26 -0
  196. package/templates/marketplace/app/sitemap.ts +14 -0
  197. package/templates/marketplace/app.ts +190 -0
  198. package/templates/marketplace/client/AuthNav.tsx +46 -0
  199. package/templates/marketplace/client/LiveTicker.tsx +104 -0
  200. package/templates/marketplace/client/LoginCard.tsx +130 -0
  201. package/templates/marketplace/client/MarketProvider.tsx +148 -0
  202. package/templates/marketplace/client/MyMarket.tsx +180 -0
  203. package/templates/marketplace/client/OfferPanel.tsx +355 -0
  204. package/templates/marketplace/client/SeedOnEmpty.tsx +26 -0
  205. package/templates/marketplace/client/SellForm.tsx +160 -0
  206. package/templates/marketplace/client/WatchButton.tsx +88 -0
  207. package/templates/marketplace/client/market.ts +341 -0
  208. package/templates/marketplace/functions/buyNow.ts +78 -0
  209. package/templates/marketplace/functions/makeOffer.ts +65 -0
  210. package/templates/marketplace/functions/respondToOffer.ts +62 -0
  211. package/templates/marketplace/functions/seedMarket.ts +90 -0
  212. package/templates/marketplace/gitignore +10 -0
  213. package/templates/marketplace/package.json +35 -0
  214. package/templates/marketplace/tsconfig.json +14 -0
  215. package/templates/marketplace/ui/badge.tsx +30 -0
  216. package/templates/marketplace/ui/button.tsx +49 -0
  217. package/templates/marketplace/ui/card.tsx +48 -0
  218. package/templates/marketplace/ui/input.tsx +17 -0
  219. package/templates/marketplace/ui/label.tsx +18 -0
  220. package/templates/marketplace/ui/textarea.tsx +17 -0
  221. package/templates/marketplace/ui/tokens.css +32 -0
  222. package/templates/marketplace/ui/utils.ts +6 -0
  223. package/templates/restaurant/.env.example +12 -0
  224. package/templates/restaurant/AGENTS.md +61 -0
  225. package/templates/restaurant/README.md +77 -0
  226. package/templates/restaurant/app/auth-form.tsx +129 -0
  227. package/templates/restaurant/app/dashboard/dashboard-client.tsx +263 -0
  228. package/templates/restaurant/app/dashboard/page.tsx +59 -0
  229. package/templates/restaurant/app/error.tsx +26 -0
  230. package/templates/restaurant/app/globals.css +148 -0
  231. package/templates/restaurant/app/layout.tsx +151 -0
  232. package/templates/restaurant/app/login/page.tsx +39 -0
  233. package/templates/restaurant/app/not-found.tsx +19 -0
  234. package/templates/restaurant/app/page.tsx +194 -0
  235. package/templates/restaurant/app/reservation-widget.tsx +359 -0
  236. package/templates/restaurant/app/robots.ts +12 -0
  237. package/templates/restaurant/app/sitemap.ts +9 -0
  238. package/templates/restaurant/app.ts +115 -0
  239. package/templates/restaurant/components/marketing.tsx +162 -0
  240. package/templates/restaurant/components/section-scroller.tsx +35 -0
  241. package/templates/restaurant/components/ui/button.tsx +56 -0
  242. package/templates/restaurant/components/ui/card.tsx +90 -0
  243. package/templates/restaurant/components.json +20 -0
  244. package/templates/restaurant/functions/cancelReservation.ts +26 -0
  245. package/templates/restaurant/functions/confirmReservation.ts +17 -0
  246. package/templates/restaurant/functions/createReservation.ts +92 -0
  247. package/templates/restaurant/functions/reservationsForOwner.ts +28 -0
  248. package/templates/restaurant/gitignore +10 -0
  249. package/templates/restaurant/lib/owner.ts +26 -0
  250. package/templates/restaurant/lib/reservation.ts +22 -0
  251. package/templates/restaurant/lib/site.config.ts +218 -0
  252. package/templates/restaurant/lib/slots.ts +55 -0
  253. package/templates/restaurant/lib/utils.ts +10 -0
  254. package/templates/restaurant/package.json +34 -0
  255. package/templates/restaurant/tsconfig.json +18 -0
  256. package/templates/shop/.env.example +32 -0
  257. package/templates/shop/AGENTS.md +61 -0
  258. package/templates/shop/README.md +102 -0
  259. package/templates/shop/app/auth-form.tsx +129 -0
  260. package/templates/shop/app/dashboard/dashboard-client.tsx +264 -0
  261. package/templates/shop/app/dashboard/page.tsx +59 -0
  262. package/templates/shop/app/error.tsx +26 -0
  263. package/templates/shop/app/globals.css +148 -0
  264. package/templates/shop/app/layout.tsx +160 -0
  265. package/templates/shop/app/login/page.tsx +39 -0
  266. package/templates/shop/app/not-found.tsx +19 -0
  267. package/templates/shop/app/page.tsx +95 -0
  268. package/templates/shop/app/robots.ts +12 -0
  269. package/templates/shop/app/shop-client.tsx +436 -0
  270. package/templates/shop/app/sitemap.ts +9 -0
  271. package/templates/shop/app/success/page.tsx +33 -0
  272. package/templates/shop/app.ts +134 -0
  273. package/templates/shop/components/marketing.tsx +96 -0
  274. package/templates/shop/components/section-scroller.tsx +35 -0
  275. package/templates/shop/components/ui/button.tsx +56 -0
  276. package/templates/shop/components/ui/card.tsx +90 -0
  277. package/templates/shop/components.json +20 -0
  278. package/templates/shop/functions/cancelOrder.ts +33 -0
  279. package/templates/shop/functions/checkout.ts +130 -0
  280. package/templates/shop/functions/fulfillOrder.ts +17 -0
  281. package/templates/shop/functions/markGroupPaid.ts +26 -0
  282. package/templates/shop/functions/ordersForOwner.ts +28 -0
  283. package/templates/shop/functions/releaseGroup.ts +36 -0
  284. package/templates/shop/functions/reserveCart.ts +87 -0
  285. package/templates/shop/functions/restockProduct.ts +23 -0
  286. package/templates/shop/functions/seedProducts.ts +30 -0
  287. package/templates/shop/functions/stripeWebhook.ts +72 -0
  288. package/templates/shop/gitignore +10 -0
  289. package/templates/shop/lib/owner.ts +26 -0
  290. package/templates/shop/lib/shop.ts +45 -0
  291. package/templates/shop/lib/site.config.ts +198 -0
  292. package/templates/shop/lib/utils.ts +10 -0
  293. package/templates/shop/package.json +35 -0
  294. package/templates/shop/tsconfig.json +18 -0
  295. package/templates/waitlist/.env.example +12 -0
  296. package/templates/waitlist/AGENTS.md +61 -0
  297. package/templates/waitlist/README.md +81 -0
  298. package/templates/waitlist/app/auth-form.tsx +129 -0
  299. package/templates/waitlist/app/dashboard/dashboard-client.tsx +297 -0
  300. package/templates/waitlist/app/dashboard/page.tsx +70 -0
  301. package/templates/waitlist/app/error.tsx +26 -0
  302. package/templates/waitlist/app/globals.css +148 -0
  303. package/templates/waitlist/app/layout.tsx +158 -0
  304. package/templates/waitlist/app/login/page.tsx +39 -0
  305. package/templates/waitlist/app/not-found.tsx +19 -0
  306. package/templates/waitlist/app/page.tsx +119 -0
  307. package/templates/waitlist/app/robots.ts +12 -0
  308. package/templates/waitlist/app/sitemap.ts +9 -0
  309. package/templates/waitlist/app/waitlist-hero.tsx +219 -0
  310. package/templates/waitlist/app.ts +134 -0
  311. package/templates/waitlist/components/marketing.tsx +96 -0
  312. package/templates/waitlist/components/ui/button.tsx +56 -0
  313. package/templates/waitlist/components/ui/card.tsx +90 -0
  314. package/templates/waitlist/components.json +20 -0
  315. package/templates/waitlist/functions/joinWaitlist.ts +82 -0
  316. package/templates/waitlist/functions/waitlistStats.ts +75 -0
  317. package/templates/waitlist/gitignore +10 -0
  318. package/templates/waitlist/lib/owner.ts +26 -0
  319. package/templates/waitlist/lib/site.config.ts +178 -0
  320. package/templates/waitlist/lib/stats.ts +30 -0
  321. package/templates/waitlist/lib/utils.ts +10 -0
  322. package/templates/waitlist/package.json +34 -0
  323. package/templates/waitlist/tsconfig.json +18 -0
@@ -0,0 +1,56 @@
1
+ import * as React from "react";
2
+ import { Slot } from "@radix-ui/react-slot";
3
+ import { cva, type VariantProps } from "class-variance-authority";
4
+
5
+ import { cn } from "@/lib/utils";
6
+
7
+ const buttonVariants = cva(
8
+ "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
9
+ {
10
+ variants: {
11
+ variant: {
12
+ default: "bg-primary text-primary-foreground hover:bg-primary/90",
13
+ destructive:
14
+ "bg-destructive text-white hover:bg-destructive/90",
15
+ outline:
16
+ "border border-input bg-background hover:bg-accent hover:text-accent-foreground",
17
+ secondary:
18
+ "bg-secondary text-secondary-foreground hover:bg-secondary/80",
19
+ ghost: "hover:bg-accent hover:text-accent-foreground",
20
+ link: "text-primary underline-offset-4 hover:underline",
21
+ },
22
+ size: {
23
+ default: "h-9 px-4 py-2",
24
+ sm: "h-8 rounded-md px-3 text-xs",
25
+ lg: "h-10 rounded-md px-8",
26
+ icon: "h-9 w-9",
27
+ },
28
+ },
29
+ defaultVariants: {
30
+ variant: "default",
31
+ size: "default",
32
+ },
33
+ },
34
+ );
35
+
36
+ export interface ButtonProps
37
+ extends React.ButtonHTMLAttributes<HTMLButtonElement>,
38
+ VariantProps<typeof buttonVariants> {
39
+ asChild?: boolean;
40
+ }
41
+
42
+ const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
43
+ ({ className, variant, size, asChild = false, ...props }, ref) => {
44
+ const Comp = asChild ? Slot : "button";
45
+ return (
46
+ <Comp
47
+ className={cn(buttonVariants({ variant, size, className }))}
48
+ ref={ref}
49
+ {...props}
50
+ />
51
+ );
52
+ },
53
+ );
54
+ Button.displayName = "Button";
55
+
56
+ export { Button, buttonVariants };
@@ -0,0 +1,90 @@
1
+ import * as React from "react";
2
+
3
+ import { cn } from "@/lib/utils";
4
+
5
+ function Card({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) {
6
+ return (
7
+ <div
8
+ data-slot="card"
9
+ className={cn(
10
+ "rounded-xl border bg-card text-card-foreground shadow-sm",
11
+ className,
12
+ )}
13
+ {...props}
14
+ />
15
+ );
16
+ }
17
+
18
+ function CardHeader({
19
+ className,
20
+ ...props
21
+ }: React.HTMLAttributes<HTMLDivElement>) {
22
+ return (
23
+ <div
24
+ data-slot="card-header"
25
+ className={cn("flex flex-col space-y-1.5 p-6", className)}
26
+ {...props}
27
+ />
28
+ );
29
+ }
30
+
31
+ function CardTitle({
32
+ className,
33
+ ...props
34
+ }: React.HTMLAttributes<HTMLDivElement>) {
35
+ return (
36
+ <div
37
+ data-slot="card-title"
38
+ className={cn("font-semibold leading-none tracking-tight", className)}
39
+ {...props}
40
+ />
41
+ );
42
+ }
43
+
44
+ function CardDescription({
45
+ className,
46
+ ...props
47
+ }: React.HTMLAttributes<HTMLDivElement>) {
48
+ return (
49
+ <div
50
+ data-slot="card-description"
51
+ className={cn("text-sm text-muted-foreground", className)}
52
+ {...props}
53
+ />
54
+ );
55
+ }
56
+
57
+ function CardContent({
58
+ className,
59
+ ...props
60
+ }: React.HTMLAttributes<HTMLDivElement>) {
61
+ return (
62
+ <div
63
+ data-slot="card-content"
64
+ className={cn("p-6 pt-0", className)}
65
+ {...props}
66
+ />
67
+ );
68
+ }
69
+
70
+ function CardFooter({
71
+ className,
72
+ ...props
73
+ }: React.HTMLAttributes<HTMLDivElement>) {
74
+ return (
75
+ <div
76
+ data-slot="card-footer"
77
+ className={cn("flex items-center p-6 pt-0", className)}
78
+ {...props}
79
+ />
80
+ );
81
+ }
82
+
83
+ export {
84
+ Card,
85
+ CardHeader,
86
+ CardFooter,
87
+ CardTitle,
88
+ CardDescription,
89
+ CardContent,
90
+ };
@@ -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,30 @@
1
+ import { query } from "@pylonsync/functions";
2
+ import { emailMatchesOwner } from "../lib/owner";
3
+ import type { BookingRow, OwnerBookingsResult } from "../lib/booking";
4
+
5
+ // bookingsForOwner — the owner's view of every appointment, INCLUDING the
6
+ // customer name/email/phone. This is the one function allowed to return that
7
+ // PII, so it's gated to the configured owner (PYLON_OWNER_EMAIL via ctx.env).
8
+ //
9
+ // The dashboard calls it with `callFn` and re-fetches whenever the live, public
10
+ // BookedSlot set changes — so new bookings + cancellations show up without a
11
+ // refresh, while the contact details themselves never travel over entity sync.
12
+ export default query({
13
+ auth: "user",
14
+ async handler(ctx): Promise<OwnerBookingsResult> {
15
+ const me = await ctx.db.get("User", ctx.auth.userId);
16
+ const email = (me?.email as string | undefined) ?? null;
17
+ if (!emailMatchesOwner(email, ctx.env.PYLON_OWNER_EMAIL)) {
18
+ return { authorized: false };
19
+ }
20
+
21
+ // Booking denies all client reads; the owner-only full list goes through
22
+ // the intentional cross-user read surface. Chronological by start.
23
+ const rows = (await ctx.db.unsafe.list("Booking")) as unknown as BookingRow[];
24
+ const bookings = rows
25
+ .map((r) => ({ ...r }))
26
+ .sort((a, b) => (a.startsAt < b.startsAt ? -1 : a.startsAt > b.startsAt ? 1 : 0));
27
+
28
+ return { authorized: true, bookings };
29
+ },
30
+ });
@@ -0,0 +1,27 @@
1
+ import { mutation, v } from "@pylonsync/functions";
2
+ import { emailMatchesOwner } from "../lib/owner";
3
+
4
+ // cancelBooking — owner-only. Marks the booking cancelled AND deletes its
5
+ // BookedSlot projection, which FREES the time: the deletion syncs to every open
6
+ // picker, so the slot becomes bookable again live.
7
+ export default mutation<{ bookingId: string }, { ok: boolean }>({
8
+ auth: "user",
9
+ args: { bookingId: v.id("Booking") },
10
+ async handler(ctx, args) {
11
+ const me = await ctx.db.get("User", ctx.auth.userId);
12
+ if (!emailMatchesOwner(me?.email as string | undefined, ctx.env.PYLON_OWNER_EMAIL)) {
13
+ throw ctx.error("POLICY_DENIED", "Only the owner can manage bookings.");
14
+ }
15
+ await ctx.db.unsafe.update("Booking", args.bookingId, { status: "cancelled" });
16
+
17
+ // Free the slot — find the projection row pointing at this booking.
18
+ const slots = (await ctx.db.unsafe.list("BookedSlot")) as unknown as {
19
+ id: string;
20
+ bookingId: string;
21
+ }[];
22
+ const slot = slots.find((s) => s.bookingId === args.bookingId);
23
+ if (slot) await ctx.db.unsafe.delete("BookedSlot", slot.id);
24
+
25
+ return { ok: true };
26
+ },
27
+ });
@@ -0,0 +1,18 @@
1
+ import { mutation, v } from "@pylonsync/functions";
2
+ import { emailMatchesOwner } from "../lib/owner";
3
+
4
+ // confirmBooking — owner-only. Marks a pending booking confirmed. The BookedSlot
5
+ // already exists (createBooking wrote it), so the time stays held. Mutations DO
6
+ // have `ctx.error`, so the non-owner deny throws a typed error here.
7
+ export default mutation<{ bookingId: string }, { ok: boolean }>({
8
+ auth: "user",
9
+ args: { bookingId: v.id("Booking") },
10
+ async handler(ctx, args) {
11
+ const me = await ctx.db.get("User", ctx.auth.userId);
12
+ if (!emailMatchesOwner(me?.email as string | undefined, ctx.env.PYLON_OWNER_EMAIL)) {
13
+ throw ctx.error("POLICY_DENIED", "Only the owner can manage bookings.");
14
+ }
15
+ await ctx.db.unsafe.update("Booking", args.bookingId, { status: "confirmed" });
16
+ return { ok: true };
17
+ },
18
+ });
@@ -0,0 +1,98 @@
1
+ import { mutation, v } from "@pylonsync/functions";
2
+ import { siteConfig } from "../lib/site.config";
3
+ import { rangesOverlap } from "../lib/slots";
4
+
5
+ const EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
6
+
7
+ // createBooking — the ONLY way a Booking + its BookedSlot projection are
8
+ // written. It's a `mutation` (transactional, has `ctx.db`): one atomic write of
9
+ // the PII Booking row plus the PII-free BookedSlot, and the BookedSlot insert
10
+ // fires a change event that greys the slot out on every open picker in
11
+ // realtime.
12
+ //
13
+ // `auth: "public"` — a visitor has no account. The server RE-CHECKS that the
14
+ // slot is still free before inserting (the live UI mostly prevents collisions,
15
+ // but two people can hit "book" within the same instant). A per-day advisory
16
+ // lock serializes the check-then-insert so the re-check can't race — including
17
+ // on Postgres, where mutations can otherwise interleave.
18
+ //
19
+ // PRIVACY: returns only `{ ok, reason? }` — never a booking row or anyone's
20
+ // contact details.
21
+ export default mutation<
22
+ {
23
+ serviceSlug: string;
24
+ startsAt: string;
25
+ customerName: string;
26
+ customerEmail: string;
27
+ customerPhone?: string;
28
+ },
29
+ { ok: boolean; reason?: "past" | "taken" | "invalid" }
30
+ >({
31
+ auth: "public",
32
+ args: {
33
+ serviceSlug: v.string(),
34
+ startsAt: v.string(),
35
+ customerName: v.string(),
36
+ customerEmail: v.string(),
37
+ customerPhone: v.optional(v.string()),
38
+ },
39
+ async handler(ctx, args) {
40
+ const svc = siteConfig.services.items.find((s) => s.slug === args.serviceSlug);
41
+ if (!svc) throw ctx.error("INVALID_ARGS", "Unknown service.");
42
+
43
+ const name = args.customerName.trim();
44
+ const email = args.customerEmail.trim().toLowerCase();
45
+ const phone = args.customerPhone?.trim() || null;
46
+ if (name.length < 1 || name.length > 120) {
47
+ throw ctx.error("INVALID_ARGS", "Enter your name.");
48
+ }
49
+ if (!EMAIL_RE.test(email) || email.length > 254) {
50
+ throw ctx.error("INVALID_ARGS", "Enter a valid email address.");
51
+ }
52
+
53
+ const startMs = Date.parse(args.startsAt);
54
+ if (Number.isNaN(startMs)) return { ok: false, reason: "invalid" };
55
+ const endMs = startMs + svc.durationMin * 60_000;
56
+ const startsAt = new Date(startMs).toISOString();
57
+ const endsAt = new Date(endMs).toISOString();
58
+
59
+ // Must be far enough in the future.
60
+ const minStart = Date.now() + siteConfig.booking.leadTimeHours * 3_600_000;
61
+ if (startMs < minStart) return { ok: false, reason: "past" };
62
+
63
+ // Serialize the check-then-insert for this calendar day so two concurrent
64
+ // bookings can't both pass the overlap check. Held until this tx commits.
65
+ await ctx.db.advisoryLock(`booking_day:${startsAt.slice(0, 10)}`);
66
+
67
+ // Re-check overlap against existing busy slots (cross-user read → unsafe).
68
+ const busy = (await ctx.db.unsafe.list("BookedSlot")) as unknown as {
69
+ startsAt: string;
70
+ endsAt: string;
71
+ }[];
72
+ const conflict = busy.some((b) =>
73
+ rangesOverlap(startMs, endMs, Date.parse(b.startsAt), Date.parse(b.endsAt)),
74
+ );
75
+ if (conflict) return { ok: false, reason: "taken" };
76
+
77
+ // Booking denies all client access by policy; createBooking is the only
78
+ // writer, so these go through the trusted `unsafe` surface.
79
+ const bookingId = await ctx.db.unsafe.insert("Booking", {
80
+ serviceSlug: svc.slug,
81
+ startsAt,
82
+ endsAt,
83
+ customerName: name,
84
+ customerEmail: email,
85
+ customerPhone: phone,
86
+ status: "pending",
87
+ createdAt: new Date().toISOString(),
88
+ });
89
+ await ctx.db.unsafe.insert("BookedSlot", {
90
+ serviceSlug: svc.slug,
91
+ startsAt,
92
+ endsAt,
93
+ bookingId,
94
+ });
95
+
96
+ return { ok: true };
97
+ },
98
+ });
@@ -0,0 +1,10 @@
1
+ node_modules/
2
+ .pylon/
3
+ pylon.manifest.json
4
+ pylon.client.ts
5
+ web/dist/
6
+ *.db
7
+ *.db-*
8
+ .env
9
+ .env.local
10
+ .DS_Store
@@ -0,0 +1,24 @@
1
+ // Shape of a booking row as the owner dashboard sees it, shared by the server
2
+ // query (functions/bookingsForOwner.ts) and the client view
3
+ // (app/dashboard/dashboard-client.tsx). The client imports only the type, never
4
+ // server code.
5
+
6
+ export interface BookingRow {
7
+ id: string;
8
+ serviceSlug: string;
9
+ startsAt: string;
10
+ endsAt: string;
11
+ customerName: string;
12
+ customerEmail: string;
13
+ customerPhone?: string | null;
14
+ status: string; // "pending" | "confirmed" | "cancelled"
15
+ createdAt: string;
16
+ }
17
+
18
+ // bookingsForOwner returns a discriminated result rather than throwing on a
19
+ // non-owner: a query has no `ctx.error`, and a bare throw reaches the client as
20
+ // a stripped HANDLER_ERROR. A non-owner gets `{ authorized: false }` and NO
21
+ // booking data.
22
+ export type OwnerBookingsResult =
23
+ | { authorized: true; bookings: BookingRow[] }
24
+ | { authorized: false };
@@ -0,0 +1,26 @@
1
+ // Who owns this waitlist? A waitlist is single-tenant — one business, one
2
+ // owner — so ownership is just "the email the owner signs in with", configured
3
+ // once via the PYLON_OWNER_EMAIL env var. The owner-only `waitlistStats`
4
+ // function reads that env (via `ctx.env`) and compares it here.
5
+ //
6
+ // Fail closed: if PYLON_OWNER_EMAIL is unset, NOBODY is the owner and the
7
+ // dashboard stays locked. That's deliberate — an unset owner on a public site
8
+ // must not mean "everyone can read the signups". Set it in .env (see
9
+ // .env.example) before signing in.
10
+
11
+ export function normalizeOwner(raw: string | null | undefined): string | null {
12
+ const v = raw?.trim().toLowerCase();
13
+ return v && v.length > 0 ? v : null;
14
+ }
15
+
16
+ // Pure comparator — the caller supplies the configured owner value (from
17
+ // `ctx.env.PYLON_OWNER_EMAIL`), so the rule lives in one place and stays
18
+ // testable without reaching for the environment here.
19
+ export function emailMatchesOwner(
20
+ email: string | null | undefined,
21
+ ownerRaw: string | null | undefined,
22
+ ): boolean {
23
+ const owner = normalizeOwner(ownerRaw);
24
+ if (!owner) return false;
25
+ return (email ?? "").trim().toLowerCase() === owner;
26
+ }
@@ -0,0 +1,232 @@
1
+ // THE single source of truth for everything business-specific. Rebrand the
2
+ // whole site — and reconfigure the booking engine — by editing this ONE file.
3
+ // The landing page, layout, AND the createBooking server function all read from
4
+ // here, so services, prices, weekly hours, and lead time stay in lockstep. The
5
+ // create-pylon scaffolder and Mast target this file: a whole appointment site
6
+ // is themed + configured by producing one typed object.
7
+ //
8
+ // Colors live here (applied as CSS variables on <html> in app/layout.tsx).
9
+ //
10
+ // Fictional demo copy — replace the values, keep the shape.
11
+
12
+ /* ----------------------------- types ----------------------------- */
13
+
14
+ export type Social = { label: string; href: string; path: string };
15
+
16
+ export type BaseConfig = {
17
+ brand: {
18
+ name: string;
19
+ letter: string;
20
+ domain: string;
21
+ email: string;
22
+ footerBlurb: string;
23
+ copyrightName: string;
24
+ socials: Social[];
25
+ };
26
+ colors: { brand: string; brandSoft: string; paper: string };
27
+ seo: { title: string; description: string };
28
+ };
29
+
30
+ export type ServiceItem = {
31
+ slug: string;
32
+ name: string;
33
+ durationMin: number; // drives the booking slot length
34
+ price: string; // display only, e.g. "$35"
35
+ description?: string;
36
+ };
37
+
38
+ export type DayHours = { open: string; close: string } | null; // "09:00".."18:00"
39
+
40
+ export type Review = { quote: string; name: string; rating?: number };
41
+ export type Faq = { q: string; a: string };
42
+
43
+ export type LocalServiceConfig = BaseConfig & {
44
+ hero: {
45
+ tagline: string;
46
+ headline: string;
47
+ subcopy: string;
48
+ ctaLabel: string;
49
+ quickFacts: { hours: string; area: string; phone: string };
50
+ };
51
+ services: {
52
+ eyebrow: string;
53
+ headline: string;
54
+ items: ServiceItem[];
55
+ };
56
+ booking: {
57
+ enabled: boolean;
58
+ eyebrow: string;
59
+ headline: string;
60
+ subcopy: string;
61
+ slotMinutes: number; // granularity of offered start times, e.g. 30
62
+ leadTimeHours: number; // earliest bookable lead time from now
63
+ daysAhead: number; // how many days the picker offers
64
+ // Weekly hours keyed by day-of-week (0=Sun … 6=Sat). null = closed.
65
+ hours: Record<number, DayHours>;
66
+ confirmationMessage: string;
67
+ };
68
+ reviews?: { eyebrow: string; headline: string; items: Review[] };
69
+ location: {
70
+ eyebrow: string;
71
+ headline: string;
72
+ address: string;
73
+ mapEmbedUrl?: string;
74
+ hoursText: string;
75
+ phone: string;
76
+ email: string;
77
+ };
78
+ faq?: { eyebrow: string; headline: string; items: Faq[] };
79
+ };
80
+
81
+ /* ----------------------------- config ---------------------------- */
82
+
83
+ export const siteConfig: LocalServiceConfig = {
84
+ brand: {
85
+ name: "Northgate Barbers",
86
+ letter: "N",
87
+ domain: "northgatebarbers.com",
88
+ email: "hello@northgatebarbers.example",
89
+ footerBlurb:
90
+ "A neighborhood barbershop in Dallas. Classic cuts, hot-towel shaves, and a chair that's always ready. Book in ten seconds.",
91
+ copyrightName: "Northgate Barbers",
92
+ socials: [
93
+ {
94
+ label: "Instagram",
95
+ href: "https://instagram.com",
96
+ 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",
97
+ },
98
+ ],
99
+ },
100
+
101
+ colors: { brand: "#b45309", brandSoft: "#fef3c7", paper: "#fafaf9" },
102
+
103
+ seo: {
104
+ title: "Northgate Barbers — classic cuts in Dallas. Book online.",
105
+ description:
106
+ "A neighborhood barbershop in Dallas. Haircuts, beard trims, and hot-towel shaves. See live availability and book your chair in seconds.",
107
+ },
108
+
109
+ hero: {
110
+ tagline: "Dallas · est. 2014",
111
+ headline: "A proper haircut, booked in ten seconds.",
112
+ subcopy:
113
+ "Classic cuts, beard work, and hot-towel shaves from barbers who've been at it a while. Pick a time that's actually open — availability updates live — and you're set.",
114
+ ctaLabel: "Book a chair",
115
+ quickFacts: {
116
+ hours: "Tue–Sat, 9–6",
117
+ area: "Lower Greenville, Dallas",
118
+ phone: "(214) 555-0148",
119
+ },
120
+ },
121
+
122
+ services: {
123
+ eyebrow: "Services",
124
+ headline: "Simple menu, honest prices.",
125
+ items: [
126
+ {
127
+ slug: "haircut",
128
+ name: "Haircut",
129
+ durationMin: 45,
130
+ price: "$35",
131
+ description: "Consultation, cut, and a clean finish. The classic.",
132
+ },
133
+ {
134
+ slug: "beard-trim",
135
+ name: "Beard trim",
136
+ durationMin: 20,
137
+ price: "$18",
138
+ description: "Shape-up, line work, and hot-towel finish.",
139
+ },
140
+ {
141
+ slug: "cut-and-beard",
142
+ name: "Cut + beard",
143
+ durationMin: 60,
144
+ price: "$48",
145
+ description: "The full sit-down. Haircut and beard, start to finish.",
146
+ },
147
+ {
148
+ slug: "kids-cut",
149
+ name: "Kids' cut",
150
+ durationMin: 30,
151
+ price: "$22",
152
+ description: "For the under-12s. Patient barbers, no rush.",
153
+ },
154
+ ],
155
+ },
156
+
157
+ booking: {
158
+ enabled: true,
159
+ eyebrow: "Book",
160
+ headline: "Find a time that's open.",
161
+ subcopy:
162
+ "Pick a service and a day — open slots are live, so what you see is what's actually free. No account, no phone tag.",
163
+ slotMinutes: 30,
164
+ leadTimeHours: 2,
165
+ daysAhead: 14,
166
+ hours: {
167
+ 0: null, // Sun — closed
168
+ 1: null, // Mon — closed
169
+ 2: { open: "09:00", close: "18:00" },
170
+ 3: { open: "09:00", close: "18:00" },
171
+ 4: { open: "09:00", close: "19:00" },
172
+ 5: { open: "09:00", close: "19:00" },
173
+ 6: { open: "09:00", close: "16:00" }, // Sat — shorter
174
+ },
175
+ confirmationMessage:
176
+ "You're booked. We'll see you then — a reminder will go out the day before.",
177
+ },
178
+
179
+ reviews: {
180
+ eyebrow: "Reviews",
181
+ headline: "Regulars say it best.",
182
+ items: [
183
+ {
184
+ quote:
185
+ "Best fade in Dallas, and I can finally book online instead of waiting around. Booked, in, out, sharp.",
186
+ name: "Marcus B.",
187
+ rating: 5,
188
+ },
189
+ {
190
+ quote:
191
+ "Been coming for three years. Same great cut every time, and the online booking is dead simple.",
192
+ name: "Daniel R.",
193
+ rating: 5,
194
+ },
195
+ {
196
+ quote:
197
+ "Took my son for his first real haircut. Patient, friendly, and the chair was ready right on time.",
198
+ name: "Hannah K.",
199
+ rating: 5,
200
+ },
201
+ ],
202
+ },
203
+
204
+ location: {
205
+ eyebrow: "Visit",
206
+ headline: "Find us on Greenville.",
207
+ address: "1845 Greenville Ave, Dallas, TX 75206",
208
+ mapEmbedUrl: "",
209
+ hoursText: "Tue–Wed 9–6 · Thu–Fri 9–7 · Sat 9–4 · Sun–Mon closed",
210
+ phone: "(214) 555-0148",
211
+ email: "hello@northgatebarbers.example",
212
+ },
213
+
214
+ faq: {
215
+ eyebrow: "Questions",
216
+ headline: "Good to know.",
217
+ items: [
218
+ {
219
+ q: "Do you take walk-ins?",
220
+ a: "When a chair's open, sure — but booking online guarantees your time, and you can see exactly what's free.",
221
+ },
222
+ {
223
+ q: "What if I need to cancel?",
224
+ a: "Just give us a call. No charge for cancellations with a few hours' notice.",
225
+ },
226
+ {
227
+ q: "How should I pay?",
228
+ a: "Cash or card in the shop. Booking online doesn't charge you anything — you pay after the cut.",
229
+ },
230
+ ],
231
+ },
232
+ };