@pylonsync/create-pylon 0.3.274 → 0.3.276
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 +1440 -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 +249 -0
- package/templates/agency/app/robots.ts +12 -0
- package/templates/agency/app/seeder.tsx +26 -0
- package/templates/agency/app/sitemap.ts +9 -0
- package/templates/agency/app/work/[slug]/page.tsx +182 -0
- package/templates/agency/app/work/page.tsx +83 -0
- package/templates/agency/app.ts +284 -0
- package/templates/agency/components/marketing.tsx +187 -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/clientsForOwner.ts +27 -0
- package/templates/agency/functions/declineInquiry.ts +41 -0
- package/templates/agency/functions/deleteClient.ts +27 -0
- package/templates/agency/functions/deleteInvoice.ts +19 -0
- package/templates/agency/functions/deleteProject.ts +20 -0
- package/templates/agency/functions/inquiriesForOwner.ts +31 -0
- package/templates/agency/functions/invoicesForOwner.ts +27 -0
- package/templates/agency/functions/seedCapacity.ts +26 -0
- package/templates/agency/functions/seedProjects.ts +41 -0
- package/templates/agency/functions/seedStudioBackoffice.ts +74 -0
- package/templates/agency/functions/setCapacity.ts +32 -0
- package/templates/agency/functions/setInvoiceStatus.ts +27 -0
- package/templates/agency/functions/setProjectFlags.ts +35 -0
- package/templates/agency/functions/submitInquiry.ts +55 -0
- package/templates/agency/functions/upsertClient.ts +73 -0
- package/templates/agency/functions/upsertInvoice.ts +113 -0
- package/templates/agency/functions/upsertProject.ts +97 -0
- package/templates/agency/gitignore +10 -0
- package/templates/agency/lib/agency.ts +189 -0
- package/templates/agency/lib/invoice-pdf.tsx +174 -0
- package/templates/agency/lib/owner.ts +26 -0
- package/templates/agency/lib/site.config.ts +418 -0
- package/templates/agency/lib/utils.ts +10 -0
- package/templates/agency/package.json +35 -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 +727 -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/functions/deleteConversation.ts +33 -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 +357 -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,129 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import React, { useState } from "react";
|
|
4
|
+
import {
|
|
5
|
+
passwordLogin,
|
|
6
|
+
passwordRegister,
|
|
7
|
+
persistSession,
|
|
8
|
+
ApiError,
|
|
9
|
+
} from "@pylonsync/client";
|
|
10
|
+
|
|
11
|
+
// The owner's email/password form — one form, two modes. It calls the built-in
|
|
12
|
+
// auth API directly (`passwordLogin` / `passwordRegister` POST to
|
|
13
|
+
// `/api/auth/password/*`), then `persistSession` writes the freshly-minted
|
|
14
|
+
// token to local storage so the sync engine + `callFn` authenticate AS THE
|
|
15
|
+
// OWNER on the next load. This step matters here specifically: the landing page
|
|
16
|
+
// mints an anonymous guest session (for the live slot counter), and without
|
|
17
|
+
// persisting the real session that stale guest token would shadow the owner's
|
|
18
|
+
// — so the owner-only `inquiriesForOwner` call would come back as a guest and
|
|
19
|
+
// get rejected. We then do a full navigation to /dashboard so the SSR runtime
|
|
20
|
+
// re-resolves auth from the HttpOnly cookie and renders server-side.
|
|
21
|
+
//
|
|
22
|
+
// A studio is single-tenant: there's no public sign-up, just the owner creating
|
|
23
|
+
// their one account. Whoever signs in only sees data if their email matches
|
|
24
|
+
// PYLON_OWNER_EMAIL — enforced by the inquiriesForOwner function.
|
|
25
|
+
export function AuthForm() {
|
|
26
|
+
const [mode, setMode] = useState<"login" | "register">("login");
|
|
27
|
+
const [email, setEmail] = useState("");
|
|
28
|
+
const [password, setPassword] = useState("");
|
|
29
|
+
const [error, setError] = useState<string | null>(null);
|
|
30
|
+
const [pending, setPending] = useState(false);
|
|
31
|
+
|
|
32
|
+
async function onSubmit(e: React.FormEvent) {
|
|
33
|
+
e.preventDefault();
|
|
34
|
+
setError(null);
|
|
35
|
+
setPending(true);
|
|
36
|
+
try {
|
|
37
|
+
const session =
|
|
38
|
+
mode === "login"
|
|
39
|
+
? await passwordLogin({ email, password })
|
|
40
|
+
: await passwordRegister({ email, password });
|
|
41
|
+
// Make this session authoritative, replacing any anonymous guest token.
|
|
42
|
+
persistSession(session);
|
|
43
|
+
window.location.assign("/dashboard");
|
|
44
|
+
} catch (err) {
|
|
45
|
+
setError(messageFor(err));
|
|
46
|
+
setPending(false);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return (
|
|
51
|
+
<div className="space-y-5">
|
|
52
|
+
<form onSubmit={onSubmit} className="space-y-4">
|
|
53
|
+
<label className="block">
|
|
54
|
+
<span className="mb-1.5 block text-[13px] font-medium text-zinc-700">Email</span>
|
|
55
|
+
<input
|
|
56
|
+
type="email"
|
|
57
|
+
value={email}
|
|
58
|
+
onChange={(e) => setEmail(e.target.value)}
|
|
59
|
+
required
|
|
60
|
+
autoComplete="email"
|
|
61
|
+
placeholder="you@yourbusiness.com"
|
|
62
|
+
className="h-10 w-full rounded-lg border border-zinc-300 bg-white px-3 text-sm text-zinc-900 outline-none transition placeholder:text-zinc-400 focus:border-brand focus:ring-2 focus:ring-brand/20"
|
|
63
|
+
/>
|
|
64
|
+
</label>
|
|
65
|
+
<label className="block">
|
|
66
|
+
<span className="mb-1.5 block text-[13px] font-medium text-zinc-700">Password</span>
|
|
67
|
+
<input
|
|
68
|
+
type="password"
|
|
69
|
+
value={password}
|
|
70
|
+
onChange={(e) => setPassword(e.target.value)}
|
|
71
|
+
required
|
|
72
|
+
autoComplete={mode === "login" ? "current-password" : "new-password"}
|
|
73
|
+
placeholder={mode === "login" ? "Your password" : "At least 10 characters"}
|
|
74
|
+
className="h-10 w-full rounded-lg border border-zinc-300 bg-white px-3 text-sm text-zinc-900 outline-none transition placeholder:text-zinc-400 focus:border-brand focus:ring-2 focus:ring-brand/20"
|
|
75
|
+
/>
|
|
76
|
+
</label>
|
|
77
|
+
{error ? (
|
|
78
|
+
<p className="rounded-lg border border-red-200 bg-red-50 px-3 py-2 text-[13px] leading-snug text-red-700">
|
|
79
|
+
{error}
|
|
80
|
+
</p>
|
|
81
|
+
) : null}
|
|
82
|
+
<button
|
|
83
|
+
type="submit"
|
|
84
|
+
disabled={pending}
|
|
85
|
+
className="inline-flex h-10 w-full items-center justify-center rounded-lg bg-zinc-900 text-sm font-medium text-white transition-colors hover:bg-zinc-700 disabled:opacity-60"
|
|
86
|
+
>
|
|
87
|
+
{pending ? "…" : mode === "login" ? "Sign in" : "Create account"}
|
|
88
|
+
</button>
|
|
89
|
+
</form>
|
|
90
|
+
|
|
91
|
+
<p className="text-center text-[13px] text-zinc-500">
|
|
92
|
+
{mode === "login" ? "First time here?" : "Already have an account?"}{" "}
|
|
93
|
+
<button
|
|
94
|
+
type="button"
|
|
95
|
+
onClick={() => {
|
|
96
|
+
setMode(mode === "login" ? "register" : "login");
|
|
97
|
+
setError(null);
|
|
98
|
+
}}
|
|
99
|
+
className="font-medium text-zinc-900 underline underline-offset-2"
|
|
100
|
+
>
|
|
101
|
+
{mode === "login" ? "Create the owner account" : "Sign in"}
|
|
102
|
+
</button>
|
|
103
|
+
</p>
|
|
104
|
+
</div>
|
|
105
|
+
);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Map the framework's auth error codes to friendly copy. `ApiError` carries a
|
|
109
|
+
// stable `.code` so you branch on the code, not the message.
|
|
110
|
+
function messageFor(err: unknown): string {
|
|
111
|
+
if (err instanceof ApiError) {
|
|
112
|
+
switch (err.code) {
|
|
113
|
+
case "INVALID_CREDENTIALS":
|
|
114
|
+
return "Wrong email or password.";
|
|
115
|
+
case "USER_EXISTS":
|
|
116
|
+
return "That email is already registered — sign in instead.";
|
|
117
|
+
case "WEAK_PASSWORD":
|
|
118
|
+
return "Pick a longer password — at least 10 characters.";
|
|
119
|
+
case "PWNED_PASSWORD":
|
|
120
|
+
return "That password has appeared in a known data breach. Choose a different one.";
|
|
121
|
+
case "RATE_LIMITED":
|
|
122
|
+
return "Too many attempts — try again in a minute.";
|
|
123
|
+
default:
|
|
124
|
+
return err.message;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
if (err instanceof Error) return err.message;
|
|
128
|
+
return "Something went wrong. Try again.";
|
|
129
|
+
}
|
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import React, { useEffect, useState } from "react";
|
|
4
|
+
import { db, callFn } from "@pylonsync/react";
|
|
5
|
+
import { EnsureGuest } from "@pylonsync/client";
|
|
6
|
+
import { siteConfig } from "@/lib/site.config";
|
|
7
|
+
|
|
8
|
+
// The realtime pieces of the page, both driven by the public, PII-free Capacity
|
|
9
|
+
// row via `db.useQuery("Capacity")` — so when the owner books a project from the
|
|
10
|
+
// dashboard, the "N slots open" number drops live in every open tab. No refresh.
|
|
11
|
+
//
|
|
12
|
+
// • <LiveSlots> — the hero pill ("3 project slots open · Q3 2026").
|
|
13
|
+
// • <ContactForm> — the "start a project" form. submitInquiry is a public
|
|
14
|
+
// mutation, so it works for anonymous visitors; the Inquiry
|
|
15
|
+
// it writes is pure PII and can never be read back by a
|
|
16
|
+
// client. The form only ever reads the slot count.
|
|
17
|
+
//
|
|
18
|
+
// Both are wrapped in <EnsureGuest>, which mints an anonymous guest session so
|
|
19
|
+
// the sync connection (which powers the live count) is established. That session
|
|
20
|
+
// holds no PII and can't read the Inquiry table.
|
|
21
|
+
|
|
22
|
+
interface CapacityRow {
|
|
23
|
+
id: string;
|
|
24
|
+
label: string;
|
|
25
|
+
openSlots: number;
|
|
26
|
+
updatedAt: string;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function useSeedCapacity() {
|
|
30
|
+
// Create the Capacity row from config on first visit (idempotent server-side).
|
|
31
|
+
useEffect(() => {
|
|
32
|
+
void callFn("seedCapacity", {});
|
|
33
|
+
}, []);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/* ------------------------------ hero pill ------------------------------ */
|
|
37
|
+
|
|
38
|
+
export function LiveSlots() {
|
|
39
|
+
return (
|
|
40
|
+
<EnsureGuest fallback={<SlotsPill loading />}>
|
|
41
|
+
<LiveSlotsInner />
|
|
42
|
+
</EnsureGuest>
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function LiveSlotsInner() {
|
|
47
|
+
useSeedCapacity();
|
|
48
|
+
const { data, loading } = db.useQuery<CapacityRow>("Capacity");
|
|
49
|
+
const row = data[0];
|
|
50
|
+
return <SlotsPill openSlots={row?.openSlots} label={row?.label} live={!loading} />;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function SlotsPill({
|
|
54
|
+
openSlots,
|
|
55
|
+
label,
|
|
56
|
+
live,
|
|
57
|
+
loading,
|
|
58
|
+
}: {
|
|
59
|
+
openSlots?: number;
|
|
60
|
+
label?: string;
|
|
61
|
+
live?: boolean;
|
|
62
|
+
loading?: boolean;
|
|
63
|
+
}) {
|
|
64
|
+
const open = openSlots ?? siteConfig.capacity.openSlots;
|
|
65
|
+
const period = label || siteConfig.capacity.label;
|
|
66
|
+
const bookedOut = open <= 0;
|
|
67
|
+
return (
|
|
68
|
+
<span className="inline-flex items-center gap-2 rounded-full border border-zinc-200 bg-white px-3.5 py-1.5 text-[13px] font-medium text-zinc-700 shadow-sm">
|
|
69
|
+
{loading ? (
|
|
70
|
+
<span className="inline-flex size-2 rounded-full bg-zinc-300" />
|
|
71
|
+
) : (
|
|
72
|
+
<span className="relative flex size-2">
|
|
73
|
+
{live && !bookedOut ? (
|
|
74
|
+
<span className="absolute inline-flex size-2 animate-ping rounded-full bg-green-500/60" />
|
|
75
|
+
) : null}
|
|
76
|
+
<span
|
|
77
|
+
className={"relative inline-flex size-2 rounded-full " + (bookedOut ? "bg-zinc-400" : "bg-green-600")}
|
|
78
|
+
/>
|
|
79
|
+
</span>
|
|
80
|
+
)}
|
|
81
|
+
{bookedOut ? (
|
|
82
|
+
<span>Booked through {period}</span>
|
|
83
|
+
) : (
|
|
84
|
+
<span>
|
|
85
|
+
<span className="tabular-nums text-zinc-900">{open}</span>{" "}
|
|
86
|
+
{open === 1 ? "project slot" : "project slots"} open · {period}
|
|
87
|
+
</span>
|
|
88
|
+
)}
|
|
89
|
+
</span>
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/* ----------------------------- contact form ---------------------------- */
|
|
94
|
+
|
|
95
|
+
export function ContactForm() {
|
|
96
|
+
return (
|
|
97
|
+
<EnsureGuest fallback={<FormShell disabled />}>
|
|
98
|
+
<ContactFormInner />
|
|
99
|
+
</EnsureGuest>
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function ContactFormInner() {
|
|
104
|
+
useSeedCapacity();
|
|
105
|
+
const { data } = db.useQuery<CapacityRow>("Capacity");
|
|
106
|
+
const row = data[0];
|
|
107
|
+
const bookedOut = (row?.openSlots ?? siteConfig.capacity.openSlots) <= 0;
|
|
108
|
+
return <FormShell bookedOut={bookedOut} period={row?.label} />;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function FormShell({
|
|
112
|
+
disabled,
|
|
113
|
+
bookedOut,
|
|
114
|
+
period,
|
|
115
|
+
}: {
|
|
116
|
+
disabled?: boolean;
|
|
117
|
+
bookedOut?: boolean;
|
|
118
|
+
period?: string;
|
|
119
|
+
}) {
|
|
120
|
+
const { contact } = siteConfig;
|
|
121
|
+
const [form, setForm] = useState({
|
|
122
|
+
name: "",
|
|
123
|
+
email: "",
|
|
124
|
+
company: "",
|
|
125
|
+
projectType: "",
|
|
126
|
+
budget: "",
|
|
127
|
+
message: "",
|
|
128
|
+
});
|
|
129
|
+
const [status, setStatus] = useState<"idle" | "sending" | "done">("idle");
|
|
130
|
+
const [error, setError] = useState<string | null>(null);
|
|
131
|
+
|
|
132
|
+
const set = (k: keyof typeof form) => (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement>) =>
|
|
133
|
+
setForm((f) => ({ ...f, [k]: e.target.value }));
|
|
134
|
+
|
|
135
|
+
async function onSubmit(e: React.FormEvent) {
|
|
136
|
+
e.preventDefault();
|
|
137
|
+
if (status === "sending") return;
|
|
138
|
+
if (!form.name.trim() || !form.email.trim()) {
|
|
139
|
+
setError("Your name and email are required.");
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
setStatus("sending");
|
|
143
|
+
setError(null);
|
|
144
|
+
try {
|
|
145
|
+
await callFn<{ ok: boolean }>("submitInquiry", {
|
|
146
|
+
name: form.name.trim(),
|
|
147
|
+
email: form.email.trim(),
|
|
148
|
+
company: form.company.trim() || undefined,
|
|
149
|
+
projectType: form.projectType || undefined,
|
|
150
|
+
budget: form.budget || undefined,
|
|
151
|
+
message: form.message.trim() || undefined,
|
|
152
|
+
});
|
|
153
|
+
setStatus("done");
|
|
154
|
+
} catch (err) {
|
|
155
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
156
|
+
setError(
|
|
157
|
+
/valid email|INVALID_ARGS/i.test(msg)
|
|
158
|
+
? "Please enter a valid email address."
|
|
159
|
+
: "Something went wrong — try again in a moment.",
|
|
160
|
+
);
|
|
161
|
+
setStatus("idle");
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
if (status === "done") {
|
|
166
|
+
return (
|
|
167
|
+
<div className="rounded-2xl border border-brand/30 bg-brand-soft/50 px-6 py-8 text-center">
|
|
168
|
+
<div className="mx-auto flex size-10 items-center justify-center rounded-full bg-brand text-white">
|
|
169
|
+
<CheckIcon />
|
|
170
|
+
</div>
|
|
171
|
+
<p className="mt-3 text-[15px] font-semibold text-zinc-900">{contact.confirmationMessage}</p>
|
|
172
|
+
</div>
|
|
173
|
+
);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
return (
|
|
177
|
+
<form onSubmit={onSubmit} className="rounded-2xl border border-zinc-200 bg-white p-6 shadow-sm sm:p-7">
|
|
178
|
+
{bookedOut ? (
|
|
179
|
+
<p className="mb-4 rounded-lg bg-amber-50 px-3 py-2 text-[13px] text-amber-700">
|
|
180
|
+
We're fully booked{period ? ` through ${period}` : ""} — send a note anyway and we'll reach out when a slot opens.
|
|
181
|
+
</p>
|
|
182
|
+
) : null}
|
|
183
|
+
<div className="grid gap-3 sm:grid-cols-2">
|
|
184
|
+
<Field label="Name" required>
|
|
185
|
+
<input value={form.name} onChange={set("name")} autoComplete="name" aria-label="Name" className={inputCls} disabled={disabled} />
|
|
186
|
+
</Field>
|
|
187
|
+
<Field label="Email" required>
|
|
188
|
+
<input type="email" value={form.email} onChange={set("email")} autoComplete="email" aria-label="Email" className={inputCls} disabled={disabled} />
|
|
189
|
+
</Field>
|
|
190
|
+
<Field label="Company">
|
|
191
|
+
<input value={form.company} onChange={set("company")} autoComplete="organization" aria-label="Company" className={inputCls} disabled={disabled} />
|
|
192
|
+
</Field>
|
|
193
|
+
<Field label="Project type">
|
|
194
|
+
<select value={form.projectType} onChange={set("projectType")} aria-label="Project type" className={inputCls} disabled={disabled}>
|
|
195
|
+
<option value="">Select…</option>
|
|
196
|
+
{contact.projectTypes.map((t) => (
|
|
197
|
+
<option key={t} value={t}>{t}</option>
|
|
198
|
+
))}
|
|
199
|
+
</select>
|
|
200
|
+
</Field>
|
|
201
|
+
</div>
|
|
202
|
+
<div className="mt-3">
|
|
203
|
+
<Field label="Budget">
|
|
204
|
+
<select value={form.budget} onChange={set("budget")} aria-label="Budget" className={inputCls} disabled={disabled}>
|
|
205
|
+
<option value="">Select…</option>
|
|
206
|
+
{contact.budgets.map((b) => (
|
|
207
|
+
<option key={b} value={b}>{b}</option>
|
|
208
|
+
))}
|
|
209
|
+
</select>
|
|
210
|
+
</Field>
|
|
211
|
+
</div>
|
|
212
|
+
<div className="mt-3">
|
|
213
|
+
<Field label="What are you building?">
|
|
214
|
+
<textarea
|
|
215
|
+
value={form.message}
|
|
216
|
+
onChange={set("message")}
|
|
217
|
+
rows={4}
|
|
218
|
+
aria-label="Message"
|
|
219
|
+
placeholder="A sentence or two about the project, timeline, and what success looks like."
|
|
220
|
+
className={inputCls + " resize-none py-2.5"}
|
|
221
|
+
disabled={disabled}
|
|
222
|
+
/>
|
|
223
|
+
</Field>
|
|
224
|
+
</div>
|
|
225
|
+
{error ? <p className="mt-3 text-[13px] text-red-600">{error}</p> : null}
|
|
226
|
+
<button
|
|
227
|
+
type="submit"
|
|
228
|
+
disabled={status === "sending" || disabled}
|
|
229
|
+
className="mt-5 inline-flex h-11 w-full items-center justify-center rounded-full bg-brand text-[15px] font-medium text-white transition-opacity hover:opacity-90 disabled:opacity-60 sm:w-auto sm:px-7"
|
|
230
|
+
>
|
|
231
|
+
{status === "sending" ? "Sending…" : "Send inquiry"}
|
|
232
|
+
</button>
|
|
233
|
+
</form>
|
|
234
|
+
);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
function Field({ label, required, children }: { label: string; required?: boolean; children: React.ReactNode }) {
|
|
238
|
+
return (
|
|
239
|
+
<label className="block">
|
|
240
|
+
<span className="mb-1.5 block text-[12.5px] font-medium text-zinc-600">
|
|
241
|
+
{label}
|
|
242
|
+
{required ? <span className="text-brand"> *</span> : null}
|
|
243
|
+
</span>
|
|
244
|
+
{children}
|
|
245
|
+
</label>
|
|
246
|
+
);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
const inputCls =
|
|
250
|
+
"h-10 w-full rounded-lg border border-zinc-300 bg-white px-3 text-[14px] text-zinc-900 outline-none transition placeholder:text-zinc-400 focus:border-brand focus:ring-2 focus:ring-brand/20 disabled:opacity-60";
|
|
251
|
+
|
|
252
|
+
function CheckIcon() {
|
|
253
|
+
return (
|
|
254
|
+
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="3" strokeLinecap="round" strokeLinejoin="round" aria-hidden>
|
|
255
|
+
<path d="M20 6 9 17l-5-5" />
|
|
256
|
+
</svg>
|
|
257
|
+
);
|
|
258
|
+
}
|