@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,284 @@
|
|
|
1
|
+
import {
|
|
2
|
+
entity,
|
|
3
|
+
field,
|
|
4
|
+
policy,
|
|
5
|
+
auth,
|
|
6
|
+
buildManifest,
|
|
7
|
+
discoverAppRoutes,
|
|
8
|
+
} from "@pylonsync/sdk";
|
|
9
|
+
|
|
10
|
+
// ---------------------------------------------------------------------------
|
|
11
|
+
// agency — a site + back-office for a boutique studio that takes on a LIMITED
|
|
12
|
+
// number of projects at a time. The public site is a marketing page (hero,
|
|
13
|
+
// services, selected work, case studies, process, team, testimonials, contact);
|
|
14
|
+
// the owner dashboard is the studio's back-office (pipeline, portfolio, clients,
|
|
15
|
+
// invoices). The realtime hook is scarcity: the hero shows how many project
|
|
16
|
+
// slots are open this quarter, and the moment the owner books a client, that
|
|
17
|
+
// number drops for EVERYONE with the page open — no refresh.
|
|
18
|
+
//
|
|
19
|
+
// Entities, split by who can see them:
|
|
20
|
+
//
|
|
21
|
+
// PUBLIC (marketing — read by anyone, written only by the owner's functions)
|
|
22
|
+
// • Capacity — a single PII-FREE row: the booking window + open slots. Drives
|
|
23
|
+
// the live "N slots open" hero counter.
|
|
24
|
+
// • Project — a portfolio piece + case study (title, summary, tags, the
|
|
25
|
+
// challenge/approach/outcome write-up). `selected` flags the ones
|
|
26
|
+
// featured on the homepage; `published` hides drafts. Public-read
|
|
27
|
+
// so the marketing site + case-study pages render server-side.
|
|
28
|
+
//
|
|
29
|
+
// PRIVATE (back-office — deny ALL client access; owner-gated functions only)
|
|
30
|
+
// • Inquiry — a "start a project" lead (name, email, company, budget, msg).
|
|
31
|
+
// • Client — a CRM contact (name, company, email, phone, notes, status).
|
|
32
|
+
// • Invoice — a bill tied to a client (+ optional project): amount, status,
|
|
33
|
+
// issue/due dates. Money + client data, so it never leaves the
|
|
34
|
+
// owner-gated functions.
|
|
35
|
+
// • User — the studio owner's account for the dashboard.
|
|
36
|
+
// ---------------------------------------------------------------------------
|
|
37
|
+
|
|
38
|
+
// A project inquiry. Everything here is PII or commercially sensitive, so the
|
|
39
|
+
// policy denies all client access; the only way in is the submitInquiry
|
|
40
|
+
// mutation, the only way to read is the owner-gated query. `status` tracks the
|
|
41
|
+
// pipeline: "new" → "booked" (consumes a slot) | "declined".
|
|
42
|
+
const Inquiry = entity(
|
|
43
|
+
"Inquiry",
|
|
44
|
+
{
|
|
45
|
+
name: field.string(),
|
|
46
|
+
email: field.string(),
|
|
47
|
+
company: field.string().optional(),
|
|
48
|
+
projectType: field.string().optional(),
|
|
49
|
+
budget: field.string().optional(),
|
|
50
|
+
message: field.string().optional(),
|
|
51
|
+
status: field.string().default("new"), // "new" | "booked" | "declined"
|
|
52
|
+
createdAt: field.datetime().defaultNow(),
|
|
53
|
+
},
|
|
54
|
+
{ indexes: [{ name: "by_created", fields: ["createdAt"], unique: false }] },
|
|
55
|
+
);
|
|
56
|
+
|
|
57
|
+
// A single-row, PII-FREE aggregate the public page reads live. It holds only
|
|
58
|
+
// the current booking window label + the number of open project slots — no lead
|
|
59
|
+
// data. seedCapacity creates it from config on first visit; bookInquiry /
|
|
60
|
+
// setCapacity keep it current. The landing page subscribes with
|
|
61
|
+
// `db.useQuery("Capacity")`, so the "N slots open" counter ticks down across
|
|
62
|
+
// every open tab the instant the owner books someone — the cross-tab-safe
|
|
63
|
+
// realtime primitive (entity sync), not a per-connection server subscription.
|
|
64
|
+
const Capacity = entity(
|
|
65
|
+
"Capacity",
|
|
66
|
+
{
|
|
67
|
+
label: field.string().default(""), // e.g. "Q3 2026"
|
|
68
|
+
openSlots: field.int().default(0),
|
|
69
|
+
updatedAt: field.datetime().defaultNow(),
|
|
70
|
+
},
|
|
71
|
+
{},
|
|
72
|
+
);
|
|
73
|
+
|
|
74
|
+
// A portfolio piece + its case study. PUBLIC marketing content — no PII — so it
|
|
75
|
+
// reads publicly: the homepage shows the `selected` ones, /work lists every
|
|
76
|
+
// `published` one, and /work/[slug] renders the full case study, all
|
|
77
|
+
// server-side for SEO + first paint. Only the owner's functions write it
|
|
78
|
+
// (upsertProject / setProjectFlags / deleteProject / seedProjects), so a visitor
|
|
79
|
+
// can read the portfolio but never edit it.
|
|
80
|
+
//
|
|
81
|
+
// • selected — featured on the homepage "Selected work" grid. Toggling it in
|
|
82
|
+
// the dashboard re-curates the homepage.
|
|
83
|
+
// • published — false = a draft, hidden from the public site (the owner still
|
|
84
|
+
// sees it in the dashboard, which reads every row).
|
|
85
|
+
// • order — manual sort (ascending) within the grids.
|
|
86
|
+
const Project = entity(
|
|
87
|
+
"Project",
|
|
88
|
+
{
|
|
89
|
+
title: field.string(),
|
|
90
|
+
slug: field.string(), // URL segment for /work/[slug]
|
|
91
|
+
client: field.string().default(""), // display label, e.g. "Fintech · 0→1"
|
|
92
|
+
summary: field.string().default(""), // one-liner on the card
|
|
93
|
+
year: field.string().optional(), // "2026"
|
|
94
|
+
tags: field.string().optional(), // comma-separated → chips
|
|
95
|
+
selected: field.boolean().default(false), // featured on the homepage
|
|
96
|
+
published: field.boolean().default(true), // false = draft, hidden publicly
|
|
97
|
+
order: field.int().default(0), // manual sort
|
|
98
|
+
// Case-study body — three short sections render on /work/[slug].
|
|
99
|
+
challenge: field.string().optional(),
|
|
100
|
+
approach: field.string().optional(),
|
|
101
|
+
outcome: field.string().optional(),
|
|
102
|
+
liveUrl: field.string().optional(),
|
|
103
|
+
createdAt: field.datetime().defaultNow(),
|
|
104
|
+
},
|
|
105
|
+
{
|
|
106
|
+
indexes: [
|
|
107
|
+
{ name: "by_slug", fields: ["slug"], unique: true },
|
|
108
|
+
{ name: "by_created", fields: ["createdAt"], unique: false },
|
|
109
|
+
],
|
|
110
|
+
},
|
|
111
|
+
);
|
|
112
|
+
|
|
113
|
+
// A CRM contact — the people behind booked projects. Name/email/phone are PII,
|
|
114
|
+
// so it denies ALL client access; the dashboard reads + writes it only through
|
|
115
|
+
// the owner-gated functions (clientsForOwner / upsertClient / deleteClient).
|
|
116
|
+
const Client = entity(
|
|
117
|
+
"Client",
|
|
118
|
+
{
|
|
119
|
+
name: field.string(),
|
|
120
|
+
company: field.string().optional(),
|
|
121
|
+
email: field.string().optional(),
|
|
122
|
+
phone: field.string().optional(),
|
|
123
|
+
status: field.string().default("prospect"), // "prospect" | "active" | "past"
|
|
124
|
+
notes: field.string().optional(),
|
|
125
|
+
createdAt: field.datetime().defaultNow(),
|
|
126
|
+
},
|
|
127
|
+
{ indexes: [{ name: "by_created", fields: ["createdAt"], unique: false }] },
|
|
128
|
+
);
|
|
129
|
+
|
|
130
|
+
// A bill tied to a client (and optionally a project). This is MONEY plus client
|
|
131
|
+
// data, so like Client it denies all client access — invoices are owner-only,
|
|
132
|
+
// read + written through invoicesForOwner / upsertInvoice / setInvoiceStatus /
|
|
133
|
+
// deleteInvoice. `amountCents` is stored as integer cents to avoid float money
|
|
134
|
+
// bugs. `clientName` / `projectTitle` are denormalized for display so the list
|
|
135
|
+
// renders without extra joins.
|
|
136
|
+
const Invoice = entity(
|
|
137
|
+
"Invoice",
|
|
138
|
+
{
|
|
139
|
+
number: field.string(), // "INV-001"
|
|
140
|
+
clientId: field.string(),
|
|
141
|
+
clientName: field.string().default(""),
|
|
142
|
+
projectId: field.string().optional(),
|
|
143
|
+
projectTitle: field.string().optional(),
|
|
144
|
+
// Line items as a JSON-encoded array of { description, quantity, unitCents }
|
|
145
|
+
// (Pylon has no JSON column type, so it's a string). `amountCents` is the
|
|
146
|
+
// computed total, kept in sync on write so the list + totals don't have to
|
|
147
|
+
// re-parse every row. The case-study PDF + the invoice view render the items.
|
|
148
|
+
lineItems: field.string().optional(),
|
|
149
|
+
amountCents: field.int().default(0),
|
|
150
|
+
status: field.string().default("draft"), // "draft" | "sent" | "paid" | "overdue"
|
|
151
|
+
issuedAt: field.string().optional(), // ISO date "2026-06-01"
|
|
152
|
+
dueAt: field.string().optional(),
|
|
153
|
+
notes: field.string().optional(),
|
|
154
|
+
createdAt: field.datetime().defaultNow(),
|
|
155
|
+
},
|
|
156
|
+
{
|
|
157
|
+
indexes: [
|
|
158
|
+
{ name: "by_client", fields: ["clientId"], unique: false },
|
|
159
|
+
{ name: "by_created", fields: ["createdAt"], unique: false },
|
|
160
|
+
],
|
|
161
|
+
},
|
|
162
|
+
);
|
|
163
|
+
|
|
164
|
+
// The studio owner's account. Email/password auth is built in against an entity
|
|
165
|
+
// named "User" (passwordHash is server-only). The dashboard is gated to the
|
|
166
|
+
// owner — see PYLON_OWNER_EMAIL in lib/owner.ts + the owner-only functions.
|
|
167
|
+
const User = entity(
|
|
168
|
+
"User",
|
|
169
|
+
{
|
|
170
|
+
email: field.string(),
|
|
171
|
+
displayName: field.string().optional(),
|
|
172
|
+
passwordHash: field.string().serverOnly().optional(),
|
|
173
|
+
avatarColor: field.string().optional(),
|
|
174
|
+
emailVerified: field.datetime().optional(),
|
|
175
|
+
createdAt: field.datetime().defaultNow(),
|
|
176
|
+
},
|
|
177
|
+
{ indexes: [{ name: "by_email", fields: ["email"], unique: true }] },
|
|
178
|
+
);
|
|
179
|
+
|
|
180
|
+
// PRIVACY — Inquiry holds the prospect's name, email, company, and budget, so
|
|
181
|
+
// it denies EVERY client read and write. No `db.useQuery("Inquiry")` can pull a
|
|
182
|
+
// row; no client can write one directly. Writes happen only inside the
|
|
183
|
+
// submitInquiry / bookInquiry / declineInquiry mutations (functions bypass
|
|
184
|
+
// policies); reads happen only inside the owner-gated inquiriesForOwner. A
|
|
185
|
+
// studio site must never leak who's been talking to it — this guarantees it.
|
|
186
|
+
const inquiryPolicy = policy({
|
|
187
|
+
name: "inquiry_private",
|
|
188
|
+
entity: "Inquiry",
|
|
189
|
+
allowRead: "false",
|
|
190
|
+
allowInsert: "false",
|
|
191
|
+
allowUpdate: "false",
|
|
192
|
+
allowDelete: "false",
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
// The capacity row is public to READ (it's just a label + a number — the whole
|
|
196
|
+
// point is the landing page showing open slots live to everyone). Clients can't
|
|
197
|
+
// WRITE it; only seedCapacity / bookInquiry / setCapacity maintain it server-side.
|
|
198
|
+
const capacityPolicy = policy({
|
|
199
|
+
name: "capacity_public_read",
|
|
200
|
+
entity: "Capacity",
|
|
201
|
+
allowRead: "true",
|
|
202
|
+
allowInsert: "false",
|
|
203
|
+
allowUpdate: "false",
|
|
204
|
+
allowDelete: "false",
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
// Projects are PUBLIC to READ — they're marketing content (portfolio + case
|
|
208
|
+
// studies), so the site renders them server-side for SEO. Clients can't WRITE
|
|
209
|
+
// them: only the owner-gated functions do. Drafts (`published == false`) are
|
|
210
|
+
// filtered out in the public read paths (the homepage, /work, /work/[slug]);
|
|
211
|
+
// the dashboard reads every row. (We keep the read open rather than gating on
|
|
212
|
+
// `published` in the policy so the owner's dashboard can list drafts with the
|
|
213
|
+
// same live `db.useQuery` the public uses — simplest path, and a draft case
|
|
214
|
+
// study is not sensitive the way a lead or an invoice is.)
|
|
215
|
+
const projectPolicy = policy({
|
|
216
|
+
name: "project_public_read",
|
|
217
|
+
entity: "Project",
|
|
218
|
+
allowRead: "true",
|
|
219
|
+
allowInsert: "false",
|
|
220
|
+
allowUpdate: "false",
|
|
221
|
+
allowDelete: "false",
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
// Clients + Invoices are the studio's back-office: contact PII and money. Both
|
|
225
|
+
// deny ALL client access, exactly like Inquiry — they're never read or written
|
|
226
|
+
// over entity sync. The owner reaches them only through the owner-gated
|
|
227
|
+
// functions, which check PYLON_OWNER_EMAIL and use ctx.db.unsafe.
|
|
228
|
+
const clientPolicy = policy({
|
|
229
|
+
name: "client_private",
|
|
230
|
+
entity: "Client",
|
|
231
|
+
allowRead: "false",
|
|
232
|
+
allowInsert: "false",
|
|
233
|
+
allowUpdate: "false",
|
|
234
|
+
allowDelete: "false",
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
const invoicePolicy = policy({
|
|
238
|
+
name: "invoice_private",
|
|
239
|
+
entity: "Invoice",
|
|
240
|
+
allowRead: "false",
|
|
241
|
+
allowInsert: "false",
|
|
242
|
+
allowUpdate: "false",
|
|
243
|
+
allowDelete: "false",
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
const userPolicy = policy({
|
|
247
|
+
name: "user_self",
|
|
248
|
+
entity: "User",
|
|
249
|
+
allowRead: "auth.userId == data.id",
|
|
250
|
+
allowInsert: "false",
|
|
251
|
+
allowUpdate: "false",
|
|
252
|
+
allowDelete: "false",
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
const manifest = buildManifest({
|
|
256
|
+
name: "__APP_NAME__",
|
|
257
|
+
version: "0.1.0",
|
|
258
|
+
entities: [Inquiry, Capacity, Project, Client, Invoice, User],
|
|
259
|
+
// Functions live in functions/ and are discovered automatically:
|
|
260
|
+
// public: submitInquiry, seedCapacity, seedProjects
|
|
261
|
+
// owner: inquiriesForOwner, bookInquiry, declineInquiry, setCapacity,
|
|
262
|
+
// upsertProject, setProjectFlags, deleteProject,
|
|
263
|
+
// clientsForOwner, upsertClient, deleteClient,
|
|
264
|
+
// invoicesForOwner, upsertInvoice, setInvoiceStatus, deleteInvoice,
|
|
265
|
+
// seedStudioBackoffice
|
|
266
|
+
queries: [],
|
|
267
|
+
actions: [],
|
|
268
|
+
policies: [
|
|
269
|
+
inquiryPolicy,
|
|
270
|
+
capacityPolicy,
|
|
271
|
+
projectPolicy,
|
|
272
|
+
clientPolicy,
|
|
273
|
+
invoicePolicy,
|
|
274
|
+
userPolicy,
|
|
275
|
+
],
|
|
276
|
+
// Email/password is on by default against the User entity above. No orgs, no
|
|
277
|
+
// billing — a single studio is single-tenant (one business, one owner).
|
|
278
|
+
auth: auth(),
|
|
279
|
+
routes: await discoverAppRoutes(),
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
console.log(JSON.stringify(manifest, null, 2));
|
|
283
|
+
|
|
284
|
+
export default manifest;
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { Link } from "@pylonsync/react";
|
|
3
|
+
import type { ProjectView } from "@/lib/agency";
|
|
4
|
+
|
|
5
|
+
// Reusable presentational pieces for the landing page. All server-rendered —
|
|
6
|
+
// no client JS. Restyle here and the whole page follows. The brand accent
|
|
7
|
+
// (`text-brand`, `bg-brand-soft`) comes from CSS vars set on <html> in
|
|
8
|
+
// app/layout.tsx, which read lib/site.config.ts — so re-theming is one edit.
|
|
9
|
+
|
|
10
|
+
// Shared container: a contained, centered column.
|
|
11
|
+
export const WRAP = "mx-auto w-full max-w-5xl px-6";
|
|
12
|
+
|
|
13
|
+
export function Eyebrow({ children }: { children: React.ReactNode }) {
|
|
14
|
+
return (
|
|
15
|
+
<p className="font-mono text-[11px] font-semibold uppercase tracking-[0.14em] text-brand">
|
|
16
|
+
{children}
|
|
17
|
+
</p>
|
|
18
|
+
);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// "New / Coming soon"-style pill for the hero.
|
|
22
|
+
export function Badge({ children }: { children: React.ReactNode }) {
|
|
23
|
+
return (
|
|
24
|
+
<span className="inline-flex items-center gap-2 rounded-full border border-zinc-200 bg-white py-1 pl-1.5 pr-3 text-[13px] text-zinc-600 shadow-sm">
|
|
25
|
+
<span className="inline-block size-1.5 rounded-full bg-brand" />
|
|
26
|
+
{children}
|
|
27
|
+
</span>
|
|
28
|
+
);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function Divider() {
|
|
32
|
+
return (
|
|
33
|
+
<div className={WRAP}>
|
|
34
|
+
<div className="border-t border-zinc-200/70" />
|
|
35
|
+
</div>
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function SectionHead({
|
|
40
|
+
eyebrow,
|
|
41
|
+
title,
|
|
42
|
+
body,
|
|
43
|
+
}: {
|
|
44
|
+
eyebrow: string;
|
|
45
|
+
title: string;
|
|
46
|
+
body?: string;
|
|
47
|
+
}) {
|
|
48
|
+
return (
|
|
49
|
+
<div>
|
|
50
|
+
<Eyebrow>{eyebrow}</Eyebrow>
|
|
51
|
+
<h2 className="mt-4 text-balance text-2xl font-semibold leading-[1.15] tracking-[-0.02em] sm:text-3xl">
|
|
52
|
+
{title}
|
|
53
|
+
</h2>
|
|
54
|
+
{body ? (
|
|
55
|
+
<p className="mt-4 max-w-xl text-[15px] leading-relaxed text-zinc-500">
|
|
56
|
+
{body}
|
|
57
|
+
</p>
|
|
58
|
+
) : null}
|
|
59
|
+
</div>
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// A grid of value props — icon + title + body.
|
|
64
|
+
export function FeatureGrid({
|
|
65
|
+
items,
|
|
66
|
+
}: {
|
|
67
|
+
items: { title: string; body: string; icon?: string }[];
|
|
68
|
+
}) {
|
|
69
|
+
return (
|
|
70
|
+
<div className="grid gap-6 sm:grid-cols-3">
|
|
71
|
+
{items.map((f) => (
|
|
72
|
+
<div key={f.title}>
|
|
73
|
+
{f.icon ? (
|
|
74
|
+
<span className="flex size-9 items-center justify-center rounded-lg bg-brand-soft text-brand">
|
|
75
|
+
{f.icon}
|
|
76
|
+
</span>
|
|
77
|
+
) : null}
|
|
78
|
+
<h3 className="mt-4 text-[15px] font-semibold text-zinc-900">
|
|
79
|
+
{f.title}
|
|
80
|
+
</h3>
|
|
81
|
+
<p className="mt-2 text-[14px] leading-relaxed text-zinc-500">
|
|
82
|
+
{f.body}
|
|
83
|
+
</p>
|
|
84
|
+
</div>
|
|
85
|
+
))}
|
|
86
|
+
</div>
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Initials for testimonial avatars, so the cards look finished without a photo.
|
|
91
|
+
export function initials(name: string) {
|
|
92
|
+
return name
|
|
93
|
+
.split(/\s+/)
|
|
94
|
+
.map((w) => w[0])
|
|
95
|
+
.join("")
|
|
96
|
+
.slice(0, 2)
|
|
97
|
+
.toUpperCase();
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// A linked portfolio card — the project's cover, title, client label, summary,
|
|
101
|
+
// and tag chips, linking to its /work/[slug] case study. Used on the homepage
|
|
102
|
+
// "Selected work" grid and the full /work index, so both stay in lockstep.
|
|
103
|
+
export function ProjectCard({ p }: { p: ProjectView }) {
|
|
104
|
+
return (
|
|
105
|
+
<Link href={`/work/${p.slug}`} className="group block">
|
|
106
|
+
{/* Case-study cover — drop in a real project screenshot. */}
|
|
107
|
+
<ImagePlaceholder
|
|
108
|
+
shape="landscape"
|
|
109
|
+
title={`${p.title} — project shot`}
|
|
110
|
+
hint="Swap for an <img> per case study"
|
|
111
|
+
/>
|
|
112
|
+
<div className="mt-4 flex items-baseline justify-between gap-3">
|
|
113
|
+
<h3 className="text-[16px] font-semibold text-zinc-900 transition-colors group-hover:text-brand">
|
|
114
|
+
{p.title}
|
|
115
|
+
</h3>
|
|
116
|
+
<span className="shrink-0 font-mono text-[11px] uppercase tracking-wide text-zinc-400">
|
|
117
|
+
{p.client}
|
|
118
|
+
</span>
|
|
119
|
+
</div>
|
|
120
|
+
<p className="mt-1.5 text-[14px] leading-relaxed text-zinc-500">{p.summary}</p>
|
|
121
|
+
{p.tags.length > 0 ? (
|
|
122
|
+
<div className="mt-3 flex flex-wrap gap-1.5">
|
|
123
|
+
{p.tags.map((t) => (
|
|
124
|
+
<span key={t} className="rounded-full bg-zinc-100 px-2.5 py-0.5 text-[11px] font-medium text-zinc-600">
|
|
125
|
+
{t}
|
|
126
|
+
</span>
|
|
127
|
+
))}
|
|
128
|
+
</div>
|
|
129
|
+
) : null}
|
|
130
|
+
<span className="mt-3 inline-flex items-center gap-1 text-[13px] font-medium text-brand opacity-0 transition-opacity group-hover:opacity-100">
|
|
131
|
+
Read case study →
|
|
132
|
+
</span>
|
|
133
|
+
</Link>
|
|
134
|
+
);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// A deliberately-obvious image placeholder. Real sites drop a photo here; this
|
|
138
|
+
// makes the spot unmistakable — dashed border, a photo glyph, and a one-line
|
|
139
|
+
// "swap this" instruction telling you exactly what to replace and where. Looks
|
|
140
|
+
// tidy enough to demo, but no one will mistake it for a finished design.
|
|
141
|
+
//
|
|
142
|
+
// shape — "landscape" | "portrait" | "square" | "circle"
|
|
143
|
+
// title — what photo belongs here ("Your headshot")
|
|
144
|
+
// hint — how to replace it ("Replace in app/page.tsx")
|
|
145
|
+
export function ImagePlaceholder({
|
|
146
|
+
shape = "landscape",
|
|
147
|
+
title,
|
|
148
|
+
hint,
|
|
149
|
+
className = "",
|
|
150
|
+
}: {
|
|
151
|
+
shape?: "landscape" | "portrait" | "square" | "circle";
|
|
152
|
+
title: string;
|
|
153
|
+
hint?: string;
|
|
154
|
+
className?: string;
|
|
155
|
+
}) {
|
|
156
|
+
const aspect =
|
|
157
|
+
shape === "portrait"
|
|
158
|
+
? "aspect-[4/5]"
|
|
159
|
+
: shape === "square" || shape === "circle"
|
|
160
|
+
? "aspect-square"
|
|
161
|
+
: "aspect-[4/3]";
|
|
162
|
+
const radius = shape === "circle" ? "rounded-full" : "rounded-2xl";
|
|
163
|
+
return (
|
|
164
|
+
<div
|
|
165
|
+
className={`relative grid place-items-center overflow-hidden border-2 border-dashed border-zinc-300 bg-zinc-50 ${aspect} ${radius} ${className}`}
|
|
166
|
+
>
|
|
167
|
+
<div className="px-4 text-center">
|
|
168
|
+
<svg
|
|
169
|
+
className="mx-auto size-7 text-zinc-300"
|
|
170
|
+
viewBox="0 0 24 24"
|
|
171
|
+
fill="none"
|
|
172
|
+
stroke="currentColor"
|
|
173
|
+
strokeWidth="1.5"
|
|
174
|
+
strokeLinecap="round"
|
|
175
|
+
strokeLinejoin="round"
|
|
176
|
+
aria-hidden
|
|
177
|
+
>
|
|
178
|
+
<rect x="3" y="3" width="18" height="18" rx="2" />
|
|
179
|
+
<circle cx="9" cy="9" r="1.6" />
|
|
180
|
+
<path d="m21 15-4.5-4.5L7 20" />
|
|
181
|
+
</svg>
|
|
182
|
+
<p className="mt-2 text-[12.5px] font-medium text-zinc-500">{title}</p>
|
|
183
|
+
{hint ? <p className="mt-1 text-[11px] leading-snug text-zinc-400">{hint}</p> : null}
|
|
184
|
+
</div>
|
|
185
|
+
</div>
|
|
186
|
+
);
|
|
187
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useEffect } from "react";
|
|
4
|
+
|
|
5
|
+
// Makes in-page section links work. A hydrated Pylon page updates the URL for a
|
|
6
|
+
// plain `<a href="#section">` click but doesn't perform the browser's native
|
|
7
|
+
// fragment scroll, so the page jumps nowhere. This installs ONE delegated click
|
|
8
|
+
// handler that catches any same-page `#`/`/#` anchor and scrolls to it smoothly.
|
|
9
|
+
//
|
|
10
|
+
// Render it once (in the root layout). Renders nothing. Real route links should
|
|
11
|
+
// still use `<Link>` from @pylonsync/react — this only handles `#` anchors.
|
|
12
|
+
export function SectionScroller() {
|
|
13
|
+
useEffect(() => {
|
|
14
|
+
function onClick(e: MouseEvent) {
|
|
15
|
+
if (e.defaultPrevented || e.button !== 0 || e.metaKey || e.ctrlKey || e.shiftKey || e.altKey) {
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
const target = e.target as Element | null;
|
|
19
|
+
const link = target?.closest?.('a[href^="#"], a[href^="/#"]') as HTMLAnchorElement | null;
|
|
20
|
+
if (!link) return;
|
|
21
|
+
const href = link.getAttribute("href") || "";
|
|
22
|
+
const id = href.slice(href.indexOf("#") + 1);
|
|
23
|
+
if (!id) return;
|
|
24
|
+
const el = document.getElementById(id);
|
|
25
|
+
if (!el) return; // target not on this page — leave it to the browser
|
|
26
|
+
e.preventDefault();
|
|
27
|
+
el.scrollIntoView({ block: "start" });
|
|
28
|
+
history.replaceState(null, "", "#" + id);
|
|
29
|
+
}
|
|
30
|
+
document.addEventListener("click", onClick);
|
|
31
|
+
return () => document.removeEventListener("click", onClick);
|
|
32
|
+
}, []);
|
|
33
|
+
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
@@ -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
|
+
}
|