@pylonsync/create-pylon 0.3.273 → 0.3.275
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/create-pylon.js +80 -0
- package/package.json +1 -1
- package/templates/ARCHETYPES.md +339 -0
- package/templates/agency/.env.example +12 -0
- package/templates/agency/AGENTS.md +61 -0
- package/templates/agency/README.md +90 -0
- package/templates/agency/app/auth-form.tsx +129 -0
- package/templates/agency/app/contact-form.tsx +258 -0
- package/templates/agency/app/dashboard/dashboard-client.tsx +286 -0
- package/templates/agency/app/dashboard/page.tsx +70 -0
- package/templates/agency/app/error.tsx +26 -0
- package/templates/agency/app/globals.css +148 -0
- package/templates/agency/app/layout.tsx +174 -0
- package/templates/agency/app/login/page.tsx +39 -0
- package/templates/agency/app/not-found.tsx +19 -0
- package/templates/agency/app/page.tsx +207 -0
- package/templates/agency/app/robots.ts +12 -0
- package/templates/agency/app/sitemap.ts +9 -0
- package/templates/agency/app.ts +135 -0
- package/templates/agency/components/marketing.tsx +148 -0
- package/templates/agency/components/section-scroller.tsx +35 -0
- package/templates/agency/components/ui/button.tsx +56 -0
- package/templates/agency/components/ui/card.tsx +90 -0
- package/templates/agency/components.json +20 -0
- package/templates/agency/functions/bookInquiry.ts +42 -0
- package/templates/agency/functions/declineInquiry.ts +41 -0
- package/templates/agency/functions/inquiriesForOwner.ts +31 -0
- package/templates/agency/functions/seedCapacity.ts +26 -0
- package/templates/agency/functions/setCapacity.ts +32 -0
- package/templates/agency/functions/submitInquiry.ts +55 -0
- package/templates/agency/gitignore +10 -0
- package/templates/agency/lib/agency.ts +27 -0
- package/templates/agency/lib/owner.ts +26 -0
- package/templates/agency/lib/site.config.ts +239 -0
- package/templates/agency/lib/utils.ts +10 -0
- package/templates/agency/package.json +34 -0
- package/templates/agency/tsconfig.json +18 -0
- package/templates/ai-chat/.env.example +33 -0
- package/templates/ai-chat/AGENTS.md +61 -0
- package/templates/ai-chat/README.md +99 -0
- package/templates/ai-chat/app/auth-form.tsx +124 -0
- package/templates/ai-chat/app/chat-client.tsx +414 -0
- package/templates/ai-chat/app/error.tsx +26 -0
- package/templates/ai-chat/app/globals.css +148 -0
- package/templates/ai-chat/app/layout.tsx +75 -0
- package/templates/ai-chat/app/login/page.tsx +39 -0
- package/templates/ai-chat/app/not-found.tsx +19 -0
- package/templates/ai-chat/app/page.tsx +23 -0
- package/templates/ai-chat/app.ts +121 -0
- package/templates/ai-chat/components.json +20 -0
- package/templates/ai-chat/gitignore +10 -0
- package/templates/ai-chat/lib/site.config.ts +103 -0
- package/templates/ai-chat/lib/utils.ts +10 -0
- package/templates/ai-chat/package.json +34 -0
- package/templates/ai-chat/tsconfig.json +18 -0
- package/templates/ai-studio/.env.example +19 -0
- package/templates/ai-studio/AGENTS.md +61 -0
- package/templates/ai-studio/README.md +83 -0
- package/templates/ai-studio/app/auth-form.tsx +124 -0
- package/templates/ai-studio/app/error.tsx +26 -0
- package/templates/ai-studio/app/globals.css +148 -0
- package/templates/ai-studio/app/layout.tsx +75 -0
- package/templates/ai-studio/app/login/page.tsx +39 -0
- package/templates/ai-studio/app/not-found.tsx +19 -0
- package/templates/ai-studio/app/page.tsx +34 -0
- package/templates/ai-studio/app/studio-client.tsx +214 -0
- package/templates/ai-studio/app.ts +108 -0
- package/templates/ai-studio/components.json +20 -0
- package/templates/ai-studio/functions/_getGeneration.ts +25 -0
- package/templates/ai-studio/functions/_updateGeneration.ts +37 -0
- package/templates/ai-studio/functions/generate.ts +42 -0
- package/templates/ai-studio/functions/pollGeneration.ts +134 -0
- package/templates/ai-studio/gitignore +10 -0
- package/templates/ai-studio/lib/site.config.ts +80 -0
- package/templates/ai-studio/lib/studio.ts +52 -0
- package/templates/ai-studio/lib/utils.ts +10 -0
- package/templates/ai-studio/package.json +34 -0
- package/templates/ai-studio/tsconfig.json +18 -0
- package/templates/creator/.env.example +12 -0
- package/templates/creator/AGENTS.md +61 -0
- package/templates/creator/README.md +67 -0
- package/templates/creator/app/auth-form.tsx +129 -0
- package/templates/creator/app/dashboard/dashboard-client.tsx +297 -0
- package/templates/creator/app/dashboard/page.tsx +70 -0
- package/templates/creator/app/error.tsx +26 -0
- package/templates/creator/app/globals.css +148 -0
- package/templates/creator/app/layout.tsx +160 -0
- package/templates/creator/app/login/page.tsx +39 -0
- package/templates/creator/app/newsletter-signup.tsx +162 -0
- package/templates/creator/app/not-found.tsx +19 -0
- package/templates/creator/app/page.tsx +160 -0
- package/templates/creator/app/robots.ts +12 -0
- package/templates/creator/app/sitemap.ts +9 -0
- package/templates/creator/app.ts +134 -0
- package/templates/creator/components/marketing.tsx +148 -0
- package/templates/creator/components/section-scroller.tsx +35 -0
- package/templates/creator/components/ui/button.tsx +56 -0
- package/templates/creator/components/ui/card.tsx +90 -0
- package/templates/creator/components.json +20 -0
- package/templates/creator/functions/subscribe.ts +82 -0
- package/templates/creator/functions/subscriberStats.ts +75 -0
- package/templates/creator/gitignore +10 -0
- package/templates/creator/lib/owner.ts +26 -0
- package/templates/creator/lib/site.config.ts +173 -0
- package/templates/creator/lib/stats.ts +30 -0
- package/templates/creator/lib/utils.ts +10 -0
- package/templates/creator/package.json +34 -0
- package/templates/creator/tsconfig.json +18 -0
- package/templates/default/app/layout.tsx +26 -27
- package/templates/default/app/page.tsx +90 -274
- package/templates/default/lib/products.ts +9 -122
- package/templates/default/lib/site.config.ts +739 -0
- package/templates/default/lib/site.ts +14 -261
- package/templates/directory/.env.example +12 -0
- package/templates/directory/AGENTS.md +61 -0
- package/templates/directory/README.md +80 -0
- package/templates/directory/app/auth-form.tsx +129 -0
- package/templates/directory/app/dashboard/dashboard-client.tsx +205 -0
- package/templates/directory/app/dashboard/page.tsx +70 -0
- package/templates/directory/app/directory-browse.tsx +328 -0
- package/templates/directory/app/error.tsx +26 -0
- package/templates/directory/app/globals.css +148 -0
- package/templates/directory/app/layout.tsx +171 -0
- package/templates/directory/app/login/page.tsx +39 -0
- package/templates/directory/app/not-found.tsx +19 -0
- package/templates/directory/app/page.tsx +50 -0
- package/templates/directory/app/robots.ts +12 -0
- package/templates/directory/app/sitemap.ts +9 -0
- package/templates/directory/app/submit/page.tsx +30 -0
- package/templates/directory/app/submit-form.tsx +151 -0
- package/templates/directory/app.ts +146 -0
- package/templates/directory/components/marketing.tsx +148 -0
- package/templates/directory/components/section-scroller.tsx +35 -0
- package/templates/directory/components/ui/button.tsx +56 -0
- package/templates/directory/components/ui/card.tsx +90 -0
- package/templates/directory/components.json +20 -0
- package/templates/directory/functions/approveSubmission.ts +45 -0
- package/templates/directory/functions/rejectSubmission.ts +20 -0
- package/templates/directory/functions/seedListings.ts +33 -0
- package/templates/directory/functions/submissionsForOwner.ts +29 -0
- package/templates/directory/functions/submitListing.ts +63 -0
- package/templates/directory/functions/upvote.ts +24 -0
- package/templates/directory/gitignore +10 -0
- package/templates/directory/lib/directory.ts +45 -0
- package/templates/directory/lib/owner.ts +26 -0
- package/templates/directory/lib/site.config.ts +130 -0
- package/templates/directory/lib/utils.ts +10 -0
- package/templates/directory/package.json +34 -0
- package/templates/directory/tsconfig.json +18 -0
- package/templates/local-service/.env.example +12 -0
- package/templates/local-service/AGENTS.md +61 -0
- package/templates/local-service/README.md +82 -0
- package/templates/local-service/app/auth-form.tsx +129 -0
- package/templates/local-service/app/booking-widget.tsx +399 -0
- package/templates/local-service/app/dashboard/dashboard-client.tsx +304 -0
- package/templates/local-service/app/dashboard/page.tsx +63 -0
- package/templates/local-service/app/error.tsx +26 -0
- package/templates/local-service/app/globals.css +148 -0
- package/templates/local-service/app/layout.tsx +151 -0
- package/templates/local-service/app/login/page.tsx +39 -0
- package/templates/local-service/app/not-found.tsx +19 -0
- package/templates/local-service/app/page.tsx +233 -0
- package/templates/local-service/app/robots.ts +12 -0
- package/templates/local-service/app/sitemap.ts +9 -0
- package/templates/local-service/app.ts +131 -0
- package/templates/local-service/components/marketing.tsx +162 -0
- package/templates/local-service/components/section-scroller.tsx +35 -0
- package/templates/local-service/components/ui/button.tsx +56 -0
- package/templates/local-service/components/ui/card.tsx +90 -0
- package/templates/local-service/components.json +20 -0
- package/templates/local-service/functions/bookingsForOwner.ts +30 -0
- package/templates/local-service/functions/cancelBooking.ts +27 -0
- package/templates/local-service/functions/confirmBooking.ts +18 -0
- package/templates/local-service/functions/createBooking.ts +98 -0
- package/templates/local-service/gitignore +10 -0
- package/templates/local-service/lib/booking.ts +24 -0
- package/templates/local-service/lib/owner.ts +26 -0
- package/templates/local-service/lib/site.config.ts +232 -0
- package/templates/local-service/lib/slots.ts +97 -0
- package/templates/local-service/lib/utils.ts +10 -0
- package/templates/local-service/package.json +34 -0
- package/templates/local-service/tsconfig.json +18 -0
- package/templates/marketplace/.env.example +9 -0
- package/templates/marketplace/AGENTS.md +61 -0
- package/templates/marketplace/README.md +78 -0
- package/templates/marketplace/app/_components/CategoryIcon.tsx +40 -0
- package/templates/marketplace/app/error.tsx +26 -0
- package/templates/marketplace/app/globals.css +64 -0
- package/templates/marketplace/app/layout.tsx +60 -0
- package/templates/marketplace/app/listing/[id]/page.tsx +163 -0
- package/templates/marketplace/app/me/page.tsx +15 -0
- package/templates/marketplace/app/not-found.tsx +20 -0
- package/templates/marketplace/app/page.tsx +159 -0
- package/templates/marketplace/app/robots.ts +12 -0
- package/templates/marketplace/app/sell/page.tsx +26 -0
- package/templates/marketplace/app/sitemap.ts +14 -0
- package/templates/marketplace/app.ts +190 -0
- package/templates/marketplace/client/AuthNav.tsx +46 -0
- package/templates/marketplace/client/LiveTicker.tsx +104 -0
- package/templates/marketplace/client/LoginCard.tsx +130 -0
- package/templates/marketplace/client/MarketProvider.tsx +148 -0
- package/templates/marketplace/client/MyMarket.tsx +180 -0
- package/templates/marketplace/client/OfferPanel.tsx +355 -0
- package/templates/marketplace/client/SeedOnEmpty.tsx +26 -0
- package/templates/marketplace/client/SellForm.tsx +160 -0
- package/templates/marketplace/client/WatchButton.tsx +88 -0
- package/templates/marketplace/client/market.ts +341 -0
- package/templates/marketplace/functions/buyNow.ts +78 -0
- package/templates/marketplace/functions/makeOffer.ts +65 -0
- package/templates/marketplace/functions/respondToOffer.ts +62 -0
- package/templates/marketplace/functions/seedMarket.ts +90 -0
- package/templates/marketplace/gitignore +10 -0
- package/templates/marketplace/package.json +35 -0
- package/templates/marketplace/tsconfig.json +14 -0
- package/templates/marketplace/ui/badge.tsx +30 -0
- package/templates/marketplace/ui/button.tsx +49 -0
- package/templates/marketplace/ui/card.tsx +48 -0
- package/templates/marketplace/ui/input.tsx +17 -0
- package/templates/marketplace/ui/label.tsx +18 -0
- package/templates/marketplace/ui/textarea.tsx +17 -0
- package/templates/marketplace/ui/tokens.css +32 -0
- package/templates/marketplace/ui/utils.ts +6 -0
- package/templates/restaurant/.env.example +12 -0
- package/templates/restaurant/AGENTS.md +61 -0
- package/templates/restaurant/README.md +77 -0
- package/templates/restaurant/app/auth-form.tsx +129 -0
- package/templates/restaurant/app/dashboard/dashboard-client.tsx +263 -0
- package/templates/restaurant/app/dashboard/page.tsx +59 -0
- package/templates/restaurant/app/error.tsx +26 -0
- package/templates/restaurant/app/globals.css +148 -0
- package/templates/restaurant/app/layout.tsx +151 -0
- package/templates/restaurant/app/login/page.tsx +39 -0
- package/templates/restaurant/app/not-found.tsx +19 -0
- package/templates/restaurant/app/page.tsx +194 -0
- package/templates/restaurant/app/reservation-widget.tsx +359 -0
- package/templates/restaurant/app/robots.ts +12 -0
- package/templates/restaurant/app/sitemap.ts +9 -0
- package/templates/restaurant/app.ts +115 -0
- package/templates/restaurant/components/marketing.tsx +162 -0
- package/templates/restaurant/components/section-scroller.tsx +35 -0
- package/templates/restaurant/components/ui/button.tsx +56 -0
- package/templates/restaurant/components/ui/card.tsx +90 -0
- package/templates/restaurant/components.json +20 -0
- package/templates/restaurant/functions/cancelReservation.ts +26 -0
- package/templates/restaurant/functions/confirmReservation.ts +17 -0
- package/templates/restaurant/functions/createReservation.ts +92 -0
- package/templates/restaurant/functions/reservationsForOwner.ts +28 -0
- package/templates/restaurant/gitignore +10 -0
- package/templates/restaurant/lib/owner.ts +26 -0
- package/templates/restaurant/lib/reservation.ts +22 -0
- package/templates/restaurant/lib/site.config.ts +218 -0
- package/templates/restaurant/lib/slots.ts +55 -0
- package/templates/restaurant/lib/utils.ts +10 -0
- package/templates/restaurant/package.json +34 -0
- package/templates/restaurant/tsconfig.json +18 -0
- package/templates/shop/.env.example +32 -0
- package/templates/shop/AGENTS.md +61 -0
- package/templates/shop/README.md +102 -0
- package/templates/shop/app/auth-form.tsx +129 -0
- package/templates/shop/app/dashboard/dashboard-client.tsx +264 -0
- package/templates/shop/app/dashboard/page.tsx +59 -0
- package/templates/shop/app/error.tsx +26 -0
- package/templates/shop/app/globals.css +148 -0
- package/templates/shop/app/layout.tsx +160 -0
- package/templates/shop/app/login/page.tsx +39 -0
- package/templates/shop/app/not-found.tsx +19 -0
- package/templates/shop/app/page.tsx +95 -0
- package/templates/shop/app/robots.ts +12 -0
- package/templates/shop/app/shop-client.tsx +436 -0
- package/templates/shop/app/sitemap.ts +9 -0
- package/templates/shop/app/success/page.tsx +33 -0
- package/templates/shop/app.ts +134 -0
- package/templates/shop/components/marketing.tsx +96 -0
- package/templates/shop/components/section-scroller.tsx +35 -0
- package/templates/shop/components/ui/button.tsx +56 -0
- package/templates/shop/components/ui/card.tsx +90 -0
- package/templates/shop/components.json +20 -0
- package/templates/shop/functions/cancelOrder.ts +33 -0
- package/templates/shop/functions/checkout.ts +130 -0
- package/templates/shop/functions/fulfillOrder.ts +17 -0
- package/templates/shop/functions/markGroupPaid.ts +26 -0
- package/templates/shop/functions/ordersForOwner.ts +28 -0
- package/templates/shop/functions/releaseGroup.ts +36 -0
- package/templates/shop/functions/reserveCart.ts +87 -0
- package/templates/shop/functions/restockProduct.ts +23 -0
- package/templates/shop/functions/seedProducts.ts +30 -0
- package/templates/shop/functions/stripeWebhook.ts +72 -0
- package/templates/shop/gitignore +10 -0
- package/templates/shop/lib/owner.ts +26 -0
- package/templates/shop/lib/shop.ts +45 -0
- package/templates/shop/lib/site.config.ts +198 -0
- package/templates/shop/lib/utils.ts +10 -0
- package/templates/shop/package.json +35 -0
- package/templates/shop/tsconfig.json +18 -0
- package/templates/waitlist/.env.example +12 -0
- package/templates/waitlist/AGENTS.md +61 -0
- package/templates/waitlist/README.md +81 -0
- package/templates/waitlist/app/auth-form.tsx +129 -0
- package/templates/waitlist/app/dashboard/dashboard-client.tsx +297 -0
- package/templates/waitlist/app/dashboard/page.tsx +70 -0
- package/templates/waitlist/app/error.tsx +26 -0
- package/templates/waitlist/app/globals.css +148 -0
- package/templates/waitlist/app/layout.tsx +158 -0
- package/templates/waitlist/app/login/page.tsx +39 -0
- package/templates/waitlist/app/not-found.tsx +19 -0
- package/templates/waitlist/app/page.tsx +119 -0
- package/templates/waitlist/app/robots.ts +12 -0
- package/templates/waitlist/app/sitemap.ts +9 -0
- package/templates/waitlist/app/waitlist-hero.tsx +219 -0
- package/templates/waitlist/app.ts +134 -0
- package/templates/waitlist/components/marketing.tsx +96 -0
- package/templates/waitlist/components/ui/button.tsx +56 -0
- package/templates/waitlist/components/ui/card.tsx +90 -0
- package/templates/waitlist/components.json +20 -0
- package/templates/waitlist/functions/joinWaitlist.ts +82 -0
- package/templates/waitlist/functions/waitlistStats.ts +75 -0
- package/templates/waitlist/gitignore +10 -0
- package/templates/waitlist/lib/owner.ts +26 -0
- package/templates/waitlist/lib/site.config.ts +178 -0
- package/templates/waitlist/lib/stats.ts +30 -0
- package/templates/waitlist/lib/utils.ts +10 -0
- package/templates/waitlist/package.json +34 -0
- package/templates/waitlist/tsconfig.json +18 -0
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
@import "tailwindcss";
|
|
2
|
+
@import "tw-animate-css";
|
|
3
|
+
|
|
4
|
+
/* Tailwind v4 scans these globs for class names. Add more @source lines if you
|
|
5
|
+
put markup elsewhere. The @pylonsync/client line lets its components
|
|
6
|
+
(EnsureGuest, auth helpers) keep any classes they ship. */
|
|
7
|
+
@source "../app/**/*.{tsx,ts,jsx,js}";
|
|
8
|
+
@source "../components/**/*.{tsx,ts,jsx,js}";
|
|
9
|
+
@source "../lib/**/*.{tsx,ts,jsx,js}";
|
|
10
|
+
@source "../node_modules/@pylonsync/client/**/*.{tsx,ts,jsx,js}";
|
|
11
|
+
|
|
12
|
+
@custom-variant dark (&:where(.dark, .dark *));
|
|
13
|
+
|
|
14
|
+
/* shadcn/ui design tokens (new-york / zinc) + the marketing brand accent. The
|
|
15
|
+
three brand vars are defaults — app/layout.tsx overrides them from
|
|
16
|
+
lib/site.config.ts on <html>, so re-theming the whole page is one edit there. */
|
|
17
|
+
:root {
|
|
18
|
+
--radius: 0.625rem;
|
|
19
|
+
--brand: #4f46e5;
|
|
20
|
+
--brand-soft: #eef2ff;
|
|
21
|
+
--paper: #fafafa;
|
|
22
|
+
--background: oklch(1 0 0);
|
|
23
|
+
--foreground: oklch(0.141 0.005 285.823);
|
|
24
|
+
--card: oklch(1 0 0);
|
|
25
|
+
--card-foreground: oklch(0.141 0.005 285.823);
|
|
26
|
+
--popover: oklch(1 0 0);
|
|
27
|
+
--popover-foreground: oklch(0.141 0.005 285.823);
|
|
28
|
+
--primary: oklch(0.21 0.006 285.885);
|
|
29
|
+
--primary-foreground: oklch(0.985 0 0);
|
|
30
|
+
--secondary: oklch(0.967 0.001 286.375);
|
|
31
|
+
--secondary-foreground: oklch(0.21 0.006 285.885);
|
|
32
|
+
--muted: oklch(0.967 0.001 286.375);
|
|
33
|
+
--muted-foreground: oklch(0.552 0.016 285.938);
|
|
34
|
+
--accent: oklch(0.967 0.001 286.375);
|
|
35
|
+
--accent-foreground: oklch(0.21 0.006 285.885);
|
|
36
|
+
--destructive: oklch(0.577 0.245 27.325);
|
|
37
|
+
--border: oklch(0.92 0.004 286.32);
|
|
38
|
+
--input: oklch(0.92 0.004 286.32);
|
|
39
|
+
--ring: oklch(0.705 0.015 286.067);
|
|
40
|
+
--chart-1: oklch(0.646 0.222 41.116);
|
|
41
|
+
--chart-2: oklch(0.6 0.118 184.704);
|
|
42
|
+
--chart-3: oklch(0.398 0.07 227.392);
|
|
43
|
+
--chart-4: oklch(0.828 0.189 84.429);
|
|
44
|
+
--chart-5: oklch(0.769 0.188 70.08);
|
|
45
|
+
--sidebar: oklch(0.985 0 0);
|
|
46
|
+
--sidebar-foreground: oklch(0.141 0.005 285.823);
|
|
47
|
+
--sidebar-primary: oklch(0.21 0.006 285.885);
|
|
48
|
+
--sidebar-primary-foreground: oklch(0.985 0 0);
|
|
49
|
+
--sidebar-accent: oklch(0.967 0.001 286.375);
|
|
50
|
+
--sidebar-accent-foreground: oklch(0.21 0.006 285.885);
|
|
51
|
+
--sidebar-border: oklch(0.92 0.004 286.32);
|
|
52
|
+
--sidebar-ring: oklch(0.705 0.015 286.067);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
.dark {
|
|
56
|
+
--background: oklch(0.141 0.005 285.823);
|
|
57
|
+
--foreground: oklch(0.985 0 0);
|
|
58
|
+
--card: oklch(0.21 0.006 285.885);
|
|
59
|
+
--card-foreground: oklch(0.985 0 0);
|
|
60
|
+
--popover: oklch(0.21 0.006 285.885);
|
|
61
|
+
--popover-foreground: oklch(0.985 0 0);
|
|
62
|
+
--primary: oklch(0.92 0.004 286.32);
|
|
63
|
+
--primary-foreground: oklch(0.21 0.006 285.885);
|
|
64
|
+
--secondary: oklch(0.274 0.006 286.033);
|
|
65
|
+
--secondary-foreground: oklch(0.985 0 0);
|
|
66
|
+
--muted: oklch(0.274 0.006 286.033);
|
|
67
|
+
--muted-foreground: oklch(0.705 0.015 286.067);
|
|
68
|
+
--accent: oklch(0.274 0.006 286.033);
|
|
69
|
+
--accent-foreground: oklch(0.985 0 0);
|
|
70
|
+
--destructive: oklch(0.704 0.191 22.216);
|
|
71
|
+
--border: oklch(1 0 0 / 10%);
|
|
72
|
+
--input: oklch(1 0 0 / 15%);
|
|
73
|
+
--ring: oklch(0.552 0.016 285.938);
|
|
74
|
+
--chart-1: oklch(0.488 0.243 264.376);
|
|
75
|
+
--chart-2: oklch(0.696 0.17 162.48);
|
|
76
|
+
--chart-3: oklch(0.769 0.188 70.08);
|
|
77
|
+
--chart-4: oklch(0.627 0.265 303.9);
|
|
78
|
+
--chart-5: oklch(0.645 0.246 16.439);
|
|
79
|
+
--sidebar: oklch(0.21 0.006 285.885);
|
|
80
|
+
--sidebar-foreground: oklch(0.985 0 0);
|
|
81
|
+
--sidebar-primary: oklch(0.488 0.243 264.376);
|
|
82
|
+
--sidebar-primary-foreground: oklch(0.985 0 0);
|
|
83
|
+
--sidebar-accent: oklch(0.274 0.006 286.033);
|
|
84
|
+
--sidebar-accent-foreground: oklch(0.985 0 0);
|
|
85
|
+
--sidebar-border: oklch(1 0 0 / 10%);
|
|
86
|
+
--sidebar-ring: oklch(0.552 0.016 285.938);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
@theme inline {
|
|
90
|
+
--radius-sm: calc(var(--radius) - 4px);
|
|
91
|
+
--radius-md: calc(var(--radius) - 2px);
|
|
92
|
+
--radius-lg: var(--radius);
|
|
93
|
+
--radius-xl: calc(var(--radius) + 4px);
|
|
94
|
+
--color-background: var(--background);
|
|
95
|
+
--color-foreground: var(--foreground);
|
|
96
|
+
--color-card: var(--card);
|
|
97
|
+
--color-card-foreground: var(--card-foreground);
|
|
98
|
+
--color-popover: var(--popover);
|
|
99
|
+
--color-popover-foreground: var(--popover-foreground);
|
|
100
|
+
--color-primary: var(--primary);
|
|
101
|
+
--color-primary-foreground: var(--primary-foreground);
|
|
102
|
+
--color-secondary: var(--secondary);
|
|
103
|
+
--color-secondary-foreground: var(--secondary-foreground);
|
|
104
|
+
--color-muted: var(--muted);
|
|
105
|
+
--color-muted-foreground: var(--muted-foreground);
|
|
106
|
+
--color-accent: var(--accent);
|
|
107
|
+
--color-accent-foreground: var(--accent-foreground);
|
|
108
|
+
--color-destructive: var(--destructive);
|
|
109
|
+
--color-border: var(--border);
|
|
110
|
+
--color-input: var(--input);
|
|
111
|
+
--color-ring: var(--ring);
|
|
112
|
+
--color-brand: var(--brand);
|
|
113
|
+
--color-brand-soft: var(--brand-soft);
|
|
114
|
+
--color-paper: var(--paper);
|
|
115
|
+
--color-chart-1: var(--chart-1);
|
|
116
|
+
--color-chart-2: var(--chart-2);
|
|
117
|
+
--color-chart-3: var(--chart-3);
|
|
118
|
+
--color-chart-4: var(--chart-4);
|
|
119
|
+
--color-chart-5: var(--chart-5);
|
|
120
|
+
--color-sidebar: var(--sidebar);
|
|
121
|
+
--color-sidebar-foreground: var(--sidebar-foreground);
|
|
122
|
+
--color-sidebar-primary: var(--sidebar-primary);
|
|
123
|
+
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
|
|
124
|
+
--color-sidebar-accent: var(--sidebar-accent);
|
|
125
|
+
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
|
|
126
|
+
--color-sidebar-border: var(--sidebar-border);
|
|
127
|
+
--color-sidebar-ring: var(--sidebar-ring);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
@layer base {
|
|
131
|
+
*,
|
|
132
|
+
::after,
|
|
133
|
+
::before,
|
|
134
|
+
::backdrop,
|
|
135
|
+
::file-selector-button {
|
|
136
|
+
border-color: var(--color-border, currentColor);
|
|
137
|
+
outline-color: var(--color-ring);
|
|
138
|
+
}
|
|
139
|
+
body {
|
|
140
|
+
background-color: var(--color-background);
|
|
141
|
+
color: var(--color-foreground);
|
|
142
|
+
font-family: Inter, ui-sans-serif, system-ui, -apple-system, sans-serif;
|
|
143
|
+
-webkit-font-smoothing: antialiased;
|
|
144
|
+
}
|
|
145
|
+
button {
|
|
146
|
+
cursor: pointer;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { Link, type PageAuth } from "@pylonsync/react";
|
|
3
|
+
import { siteConfig } from "@/lib/site.config";
|
|
4
|
+
import { SectionScroller } from "@/components/section-scroller";
|
|
5
|
+
|
|
6
|
+
// A layout wraps every page. This marketing layout renders a slim nav up top
|
|
7
|
+
// and a footer below, both driven by lib/site.config.ts. `auth.user_id` is
|
|
8
|
+
// resolved server-side from the session cookie before any HTML is sent, so the
|
|
9
|
+
// nav shows "Dashboard" once the owner is signed in and "Sign in" otherwise —
|
|
10
|
+
// no flash, no client fetch.
|
|
11
|
+
interface LayoutProps {
|
|
12
|
+
children: React.ReactNode;
|
|
13
|
+
url: string;
|
|
14
|
+
auth: PageAuth;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export default function RootLayout({ children, url, auth }: LayoutProps) {
|
|
18
|
+
// A guest session (minted by <EnsureGuest> for the live counter) has a
|
|
19
|
+
// `guest_…` user id — that's an anonymous visitor, NOT the signed-in owner,
|
|
20
|
+
// so it shouldn't flip the nav to "Dashboard".
|
|
21
|
+
const signedIn = Boolean(auth?.user_id && !auth.user_id.startsWith("guest_"));
|
|
22
|
+
const { brand, colors } = siteConfig;
|
|
23
|
+
|
|
24
|
+
// The auth screens and the dashboard bring their own chrome, so they render
|
|
25
|
+
// bare (no marketing nav/footer). Match on the path PREFIX, not a substring.
|
|
26
|
+
const path = (url ?? "").split("?")[0];
|
|
27
|
+
const BARE_PREFIXES = ["/login", "/dashboard"];
|
|
28
|
+
const isBare = BARE_PREFIXES.some((p) => path === p || path.startsWith(p + "/"));
|
|
29
|
+
|
|
30
|
+
return (
|
|
31
|
+
<html
|
|
32
|
+
lang="en"
|
|
33
|
+
// Marketing theme colors come from the single site config. Set as inline
|
|
34
|
+
// CSS vars on <html> so they override globals.css and the whole page
|
|
35
|
+
// re-themes from one place — no CSS edit needed.
|
|
36
|
+
style={
|
|
37
|
+
{
|
|
38
|
+
"--brand": colors.brand,
|
|
39
|
+
"--brand-soft": colors.brandSoft,
|
|
40
|
+
"--paper": colors.paper,
|
|
41
|
+
} as React.CSSProperties
|
|
42
|
+
}
|
|
43
|
+
>
|
|
44
|
+
<head>
|
|
45
|
+
<meta charSet="utf-8" />
|
|
46
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
47
|
+
{/* No <title> here — each page's exported `metadata` sets it. */}
|
|
48
|
+
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
|
49
|
+
<link rel="preconnect" href="https://fonts.gstatic.com" crossOrigin="anonymous" />
|
|
50
|
+
<link
|
|
51
|
+
rel="stylesheet"
|
|
52
|
+
href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap"
|
|
53
|
+
/>
|
|
54
|
+
{/* Tailwind is compiled by Pylon from app/globals.css and injected here. */}
|
|
55
|
+
</head>
|
|
56
|
+
<body className="flex min-h-screen flex-col bg-background text-foreground antialiased">
|
|
57
|
+
<SectionScroller />
|
|
58
|
+
{isBare ? (
|
|
59
|
+
children
|
|
60
|
+
) : (
|
|
61
|
+
<>
|
|
62
|
+
<header className="sticky top-0 z-30 border-b border-zinc-200/70 bg-white/85 backdrop-blur">
|
|
63
|
+
<div className="mx-auto flex h-14 max-w-5xl items-center justify-between px-6">
|
|
64
|
+
<Link href="/" className="flex items-center gap-2">
|
|
65
|
+
<span className="flex size-6 items-center justify-center rounded-[7px] bg-zinc-900 text-[13px] font-bold text-white">
|
|
66
|
+
{brand.letter}
|
|
67
|
+
</span>
|
|
68
|
+
<span className="text-[15px] font-semibold tracking-tight text-zinc-900">
|
|
69
|
+
{brand.name}
|
|
70
|
+
</span>
|
|
71
|
+
</Link>
|
|
72
|
+
<nav className="flex items-center gap-1 sm:gap-2">
|
|
73
|
+
<a href="/#browse" className="hidden rounded-full px-3 py-1.5 text-[13px] font-medium text-zinc-600 transition-colors hover:text-zinc-900 sm:inline-flex">
|
|
74
|
+
Browse
|
|
75
|
+
</a>
|
|
76
|
+
{signedIn ? (
|
|
77
|
+
<Link
|
|
78
|
+
href="/dashboard"
|
|
79
|
+
className="inline-flex items-center rounded-full bg-zinc-900 px-3.5 py-1.5 text-[13px] font-medium text-white transition-colors hover:bg-zinc-700"
|
|
80
|
+
>
|
|
81
|
+
Dashboard
|
|
82
|
+
</Link>
|
|
83
|
+
) : (
|
|
84
|
+
<>
|
|
85
|
+
<Link
|
|
86
|
+
href="/login"
|
|
87
|
+
className="hidden rounded-full px-3 py-1.5 text-[13px] font-medium text-zinc-600 transition-colors hover:text-zinc-900 sm:inline-flex"
|
|
88
|
+
>
|
|
89
|
+
Sign in
|
|
90
|
+
</Link>
|
|
91
|
+
<Link
|
|
92
|
+
href="/submit"
|
|
93
|
+
className="inline-flex items-center rounded-full bg-brand px-3.5 py-1.5 text-[13px] font-medium text-white transition-opacity hover:opacity-90"
|
|
94
|
+
>
|
|
95
|
+
{siteConfig.hero.ctaLabel}
|
|
96
|
+
</Link>
|
|
97
|
+
</>
|
|
98
|
+
)}
|
|
99
|
+
</nav>
|
|
100
|
+
</div>
|
|
101
|
+
</header>
|
|
102
|
+
|
|
103
|
+
<main className="flex-1">{children}</main>
|
|
104
|
+
|
|
105
|
+
<SiteFooter />
|
|
106
|
+
</>
|
|
107
|
+
)}
|
|
108
|
+
</body>
|
|
109
|
+
</html>
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function SiteFooter() {
|
|
114
|
+
const { brand } = siteConfig;
|
|
115
|
+
return (
|
|
116
|
+
<footer className="border-t border-zinc-200/70 bg-white">
|
|
117
|
+
<div className="mx-auto max-w-5xl px-6 py-12">
|
|
118
|
+
<div className="flex flex-col items-start justify-between gap-6 sm:flex-row">
|
|
119
|
+
<div className="max-w-sm">
|
|
120
|
+
<Link href="/" className="inline-flex items-center gap-2">
|
|
121
|
+
<span className="flex size-6 items-center justify-center rounded-[7px] bg-zinc-900 text-[13px] font-bold text-white">
|
|
122
|
+
{brand.letter}
|
|
123
|
+
</span>
|
|
124
|
+
<span className="text-[15px] font-semibold tracking-tight text-zinc-900">
|
|
125
|
+
{brand.name}
|
|
126
|
+
</span>
|
|
127
|
+
</Link>
|
|
128
|
+
<p className="mt-3 text-[13px] leading-relaxed text-zinc-500">
|
|
129
|
+
{brand.footerBlurb}
|
|
130
|
+
</p>
|
|
131
|
+
</div>
|
|
132
|
+
<div className="flex items-center gap-4">
|
|
133
|
+
{brand.socials.map((s) => (
|
|
134
|
+
<a
|
|
135
|
+
key={s.label}
|
|
136
|
+
href={s.href}
|
|
137
|
+
aria-label={s.label}
|
|
138
|
+
className="text-zinc-400 transition-colors hover:text-zinc-900"
|
|
139
|
+
>
|
|
140
|
+
<svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor">
|
|
141
|
+
<path d={s.path} />
|
|
142
|
+
</svg>
|
|
143
|
+
</a>
|
|
144
|
+
))}
|
|
145
|
+
<a
|
|
146
|
+
href={`mailto:${brand.email}`}
|
|
147
|
+
aria-label="Email"
|
|
148
|
+
className="text-zinc-400 transition-colors hover:text-zinc-900"
|
|
149
|
+
>
|
|
150
|
+
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
|
151
|
+
<rect x="3" y="5" width="18" height="14" rx="2" />
|
|
152
|
+
<path d="m3 7 9 6 9-6" />
|
|
153
|
+
</svg>
|
|
154
|
+
</a>
|
|
155
|
+
</div>
|
|
156
|
+
</div>
|
|
157
|
+
<div className="mt-10 flex flex-col items-start justify-between gap-3 border-t border-zinc-200/70 pt-6 text-[12px] text-zinc-400 sm:flex-row sm:items-center">
|
|
158
|
+
<span>
|
|
159
|
+
© {new Date().getFullYear()} {brand.copyrightName}
|
|
160
|
+
</span>
|
|
161
|
+
<span>
|
|
162
|
+
Built with{" "}
|
|
163
|
+
<a href="https://pylonsync.com" className="font-medium text-zinc-600 hover:text-zinc-900">
|
|
164
|
+
Pylon
|
|
165
|
+
</a>
|
|
166
|
+
</span>
|
|
167
|
+
</div>
|
|
168
|
+
</div>
|
|
169
|
+
</footer>
|
|
170
|
+
);
|
|
171
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { Link, type Metadata, type PageProps } from "@pylonsync/react";
|
|
3
|
+
import { AuthForm } from "../auth-form";
|
|
4
|
+
import { siteConfig } from "@/lib/site.config";
|
|
5
|
+
|
|
6
|
+
export const metadata: Metadata = {
|
|
7
|
+
title: `Sign in — ${siteConfig.brand.name}`,
|
|
8
|
+
robots: "noindex",
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
// `app/login/page.tsx` → `/login`. The owner's sign-in. Rendered bare (the
|
|
12
|
+
// layout suppresses the marketing nav/footer for /login). Already signed in?
|
|
13
|
+
// Skip straight to the dashboard — `response.redirect` in the synchronous shell
|
|
14
|
+
// render is a real 307 before any HTML is sent.
|
|
15
|
+
export default function LoginPage({ auth, response }: PageProps) {
|
|
16
|
+
// Already signed in (a real account, not an anonymous guest)? Skip the form.
|
|
17
|
+
if (auth.user_id && !auth.user_id.startsWith("guest_")) response.redirect("/dashboard");
|
|
18
|
+
const { brand } = siteConfig;
|
|
19
|
+
return (
|
|
20
|
+
<div className="flex min-h-screen items-center justify-center bg-white px-6 py-12">
|
|
21
|
+
<div className="w-full max-w-[400px] rounded-2xl border border-zinc-200/70 p-8">
|
|
22
|
+
<Link href="/" className="inline-flex">
|
|
23
|
+
<span className="flex size-9 items-center justify-center rounded-xl bg-zinc-900 text-base font-bold text-white">
|
|
24
|
+
{brand.letter}
|
|
25
|
+
</span>
|
|
26
|
+
</Link>
|
|
27
|
+
<h1 className="mt-5 text-[22px] font-semibold tracking-tight text-zinc-900">
|
|
28
|
+
{brand.name} dashboard
|
|
29
|
+
</h1>
|
|
30
|
+
<p className="mt-1 text-[13px] text-zinc-500">
|
|
31
|
+
Sign in to manage your inquiries.
|
|
32
|
+
</p>
|
|
33
|
+
<div className="mt-6">
|
|
34
|
+
<AuthForm />
|
|
35
|
+
</div>
|
|
36
|
+
</div>
|
|
37
|
+
</div>
|
|
38
|
+
);
|
|
39
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { Link, type NotFoundProps } from "@pylonsync/react";
|
|
3
|
+
|
|
4
|
+
// `app/not-found.tsx` → rendered at HTTP 404 for any unmatched URL (and when a
|
|
5
|
+
// page calls `response.notFound()`). Hydrated, so the link is a client nav.
|
|
6
|
+
export default function NotFound(_props: NotFoundProps) {
|
|
7
|
+
return (
|
|
8
|
+
<div className="mx-auto flex min-h-[60vh] max-w-3xl flex-col items-center justify-center px-6 text-center">
|
|
9
|
+
<h1 className="text-3xl font-semibold tracking-tight">404</h1>
|
|
10
|
+
<p className="mt-2 text-zinc-500">We couldn't find that page.</p>
|
|
11
|
+
<Link
|
|
12
|
+
href="/"
|
|
13
|
+
className="mt-6 inline-flex h-10 items-center rounded-full bg-zinc-900 px-5 text-sm font-medium text-white transition-colors hover:bg-zinc-700"
|
|
14
|
+
>
|
|
15
|
+
Back home
|
|
16
|
+
</Link>
|
|
17
|
+
</div>
|
|
18
|
+
);
|
|
19
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { Link, type Metadata } from "@pylonsync/react";
|
|
3
|
+
import { WRAP } from "@/components/marketing";
|
|
4
|
+
import { DirectoryBrowse } from "./directory-browse";
|
|
5
|
+
import { siteConfig } from "@/lib/site.config";
|
|
6
|
+
|
|
7
|
+
export const metadata: Metadata = {
|
|
8
|
+
title: siteConfig.seo.title,
|
|
9
|
+
description: siteConfig.seo.description,
|
|
10
|
+
openGraph: { title: siteConfig.seo.title, description: siteConfig.seo.description, type: "website" },
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
// `app/page.tsx` → `/`. Server-rendered hero + a client island (#browse) that
|
|
14
|
+
// runs a LIVE faceted full-text search over the public Listing table. Copy
|
|
15
|
+
// comes from siteConfig; the listings seed on first visit. Doesn't read `auth`,
|
|
16
|
+
// so the public page stays cacheable.
|
|
17
|
+
export default function LandingPage() {
|
|
18
|
+
const { hero, browse } = siteConfig;
|
|
19
|
+
return (
|
|
20
|
+
<div className="bg-white text-zinc-900">
|
|
21
|
+
{/* HERO */}
|
|
22
|
+
<section className={`${WRAP} pt-16 pb-8 sm:pt-20`}>
|
|
23
|
+
<div className="max-w-2xl">
|
|
24
|
+
<p className="font-mono text-[11px] uppercase tracking-[0.16em] text-brand">{hero.tagline}</p>
|
|
25
|
+
<h1 className="mt-4 text-balance text-[2.5rem] font-semibold leading-[1.04] tracking-[-0.02em] sm:text-[3.25rem]">
|
|
26
|
+
{hero.headline}
|
|
27
|
+
</h1>
|
|
28
|
+
<p className="mt-5 text-[17px] leading-relaxed text-zinc-500">{hero.subcopy}</p>
|
|
29
|
+
<div className="mt-7 flex flex-wrap items-center gap-4">
|
|
30
|
+
<a href="#browse" className="inline-flex items-center rounded-full bg-brand px-5 py-2.5 text-sm font-medium text-white transition-opacity hover:opacity-90">
|
|
31
|
+
Browse the directory
|
|
32
|
+
</a>
|
|
33
|
+
<Link href="/submit" className="text-sm font-medium text-zinc-700 hover:text-zinc-900">
|
|
34
|
+
{hero.ctaLabel} →
|
|
35
|
+
</Link>
|
|
36
|
+
</div>
|
|
37
|
+
</div>
|
|
38
|
+
</section>
|
|
39
|
+
|
|
40
|
+
{/* BROWSE — live faceted search */}
|
|
41
|
+
<section id="browse" className={`${WRAP} pb-20 pt-4`}>
|
|
42
|
+
<div className="mb-6">
|
|
43
|
+
<p className="font-mono text-[11px] uppercase tracking-[0.14em] text-brand">{browse.eyebrow}</p>
|
|
44
|
+
<h2 className="mt-3 text-2xl font-semibold tracking-[-0.02em]">{browse.headline}</h2>
|
|
45
|
+
</div>
|
|
46
|
+
<DirectoryBrowse />
|
|
47
|
+
</section>
|
|
48
|
+
</div>
|
|
49
|
+
);
|
|
50
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { Robots } from "@pylonsync/react";
|
|
2
|
+
|
|
3
|
+
// app/robots.ts → served at /robots.txt. Point SITE_URL at your domain in prod.
|
|
4
|
+
const SITE = process.env.SITE_URL ?? "http://localhost:4321";
|
|
5
|
+
|
|
6
|
+
export default function robots(): Robots {
|
|
7
|
+
return {
|
|
8
|
+
// Keep the owner dashboard and the API out of the index.
|
|
9
|
+
rules: { userAgent: "*", allow: "/", disallow: ["/dashboard", "/login", "/api/"] },
|
|
10
|
+
sitemap: `${SITE}/sitemap.xml`,
|
|
11
|
+
};
|
|
12
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { Sitemap } from "@pylonsync/react";
|
|
2
|
+
|
|
3
|
+
// app/sitemap.ts → served at /sitemap.xml. Point SITE_URL at your domain in
|
|
4
|
+
// production. The studio site is a single public page, so the sitemap is just "/".
|
|
5
|
+
const SITE = process.env.SITE_URL ?? "http://localhost:4321";
|
|
6
|
+
|
|
7
|
+
export default async function sitemap(): Promise<Sitemap> {
|
|
8
|
+
return [{ url: `${SITE}/`, changeFrequency: "weekly", priority: 1 }];
|
|
9
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { type Metadata } from "@pylonsync/react";
|
|
3
|
+
import { WRAP } from "@/components/marketing";
|
|
4
|
+
import { SubmitForm } from "../submit-form";
|
|
5
|
+
import { siteConfig } from "@/lib/site.config";
|
|
6
|
+
|
|
7
|
+
export const metadata: Metadata = {
|
|
8
|
+
title: `Submit a tool — ${siteConfig.brand.name}`,
|
|
9
|
+
description: siteConfig.submit.subcopy,
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
// `app/submit/page.tsx` → `/submit`. Server-rendered shell; the form is a client
|
|
13
|
+
// island that calls the public submitListing mutation. Submissions land in the
|
|
14
|
+
// owner's moderation queue (the deny-all Submission table) — not the public
|
|
15
|
+
// directory — until approved.
|
|
16
|
+
export default function SubmitPage() {
|
|
17
|
+
const { submit } = siteConfig;
|
|
18
|
+
return (
|
|
19
|
+
<section className={`${WRAP} py-16`}>
|
|
20
|
+
<div className="mx-auto max-w-2xl">
|
|
21
|
+
<p className="font-mono text-[11px] uppercase tracking-[0.16em] text-brand">{submit.eyebrow}</p>
|
|
22
|
+
<h1 className="mt-4 text-balance text-3xl font-semibold tracking-[-0.02em]">{submit.headline}</h1>
|
|
23
|
+
<p className="mt-4 text-[15px] leading-relaxed text-zinc-500">{submit.subcopy}</p>
|
|
24
|
+
<div className="mt-8">
|
|
25
|
+
<SubmitForm />
|
|
26
|
+
</div>
|
|
27
|
+
</div>
|
|
28
|
+
</section>
|
|
29
|
+
);
|
|
30
|
+
}
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import React, { useState } from "react";
|
|
4
|
+
import { callFn } from "@pylonsync/react";
|
|
5
|
+
import { siteConfig } from "@/lib/site.config";
|
|
6
|
+
|
|
7
|
+
// The "submit a tool" form. submitListing is a public mutation, so it works for
|
|
8
|
+
// anonymous visitors with no session. PRIVACY: the submission (incl. the
|
|
9
|
+
// submitter's email) is written to the deny-all Submission table — it is NOT
|
|
10
|
+
// public and never syncs to a browser. It shows up in the directory only after
|
|
11
|
+
// the owner approves it from the dashboard.
|
|
12
|
+
export function SubmitForm() {
|
|
13
|
+
const { submit, categories } = siteConfig;
|
|
14
|
+
const [form, setForm] = useState({
|
|
15
|
+
submitterName: "",
|
|
16
|
+
submitterEmail: "",
|
|
17
|
+
name: "",
|
|
18
|
+
tagline: "",
|
|
19
|
+
url: "",
|
|
20
|
+
category: categories[0] ?? "",
|
|
21
|
+
tags: "",
|
|
22
|
+
description: "",
|
|
23
|
+
});
|
|
24
|
+
const [status, setStatus] = useState<"idle" | "sending" | "done">("idle");
|
|
25
|
+
const [error, setError] = useState<string | null>(null);
|
|
26
|
+
|
|
27
|
+
const set =
|
|
28
|
+
(k: keyof typeof form) =>
|
|
29
|
+
(e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement>) =>
|
|
30
|
+
setForm((f) => ({ ...f, [k]: e.target.value }));
|
|
31
|
+
|
|
32
|
+
async function onSubmit(e: React.FormEvent) {
|
|
33
|
+
e.preventDefault();
|
|
34
|
+
if (status === "sending") return;
|
|
35
|
+
if (!form.submitterName.trim() || !form.submitterEmail.trim() || !form.name.trim() || !form.url.trim()) {
|
|
36
|
+
setError("Your name + email and the tool's name + URL are required.");
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
setStatus("sending");
|
|
40
|
+
setError(null);
|
|
41
|
+
try {
|
|
42
|
+
await callFn<{ ok: boolean }>("submitListing", {
|
|
43
|
+
submitterName: form.submitterName.trim(),
|
|
44
|
+
submitterEmail: form.submitterEmail.trim(),
|
|
45
|
+
name: form.name.trim(),
|
|
46
|
+
tagline: form.tagline.trim(),
|
|
47
|
+
url: form.url.trim(),
|
|
48
|
+
category: form.category,
|
|
49
|
+
tags: form.tags.trim() || undefined,
|
|
50
|
+
description: form.description.trim() || undefined,
|
|
51
|
+
});
|
|
52
|
+
setStatus("done");
|
|
53
|
+
} catch (err) {
|
|
54
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
55
|
+
setError(
|
|
56
|
+
/valid email/i.test(msg)
|
|
57
|
+
? "Enter a valid email address."
|
|
58
|
+
: /valid URL/i.test(msg)
|
|
59
|
+
? "Enter a valid URL (https://…)."
|
|
60
|
+
: /INVALID_ARGS/i.test(msg)
|
|
61
|
+
? "Please fill in the required fields."
|
|
62
|
+
: "Something went wrong — try again in a moment.",
|
|
63
|
+
);
|
|
64
|
+
setStatus("idle");
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if (status === "done") {
|
|
69
|
+
return (
|
|
70
|
+
<div className="rounded-2xl border border-brand/30 bg-brand-soft/50 px-6 py-10 text-center">
|
|
71
|
+
<div className="mx-auto flex size-10 items-center justify-center rounded-full bg-brand text-white">✓</div>
|
|
72
|
+
<p className="mt-3 text-[15px] font-semibold text-zinc-900">{submit.confirmationMessage}</p>
|
|
73
|
+
<a href="/" className="mt-5 inline-flex h-10 items-center rounded-full border border-zinc-300 px-5 text-sm font-medium text-zinc-700 hover:bg-zinc-50">
|
|
74
|
+
Back to the directory
|
|
75
|
+
</a>
|
|
76
|
+
</div>
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return (
|
|
81
|
+
<form onSubmit={onSubmit} className="rounded-2xl border border-zinc-200 bg-white p-6 shadow-sm sm:p-7">
|
|
82
|
+
<div className="grid gap-3 sm:grid-cols-2">
|
|
83
|
+
<Field label="Your name" required>
|
|
84
|
+
<input value={form.submitterName} onChange={set("submitterName")} autoComplete="name" className={inputCls} />
|
|
85
|
+
</Field>
|
|
86
|
+
<Field label="Your email" required>
|
|
87
|
+
<input type="email" value={form.submitterEmail} onChange={set("submitterEmail")} autoComplete="email" className={inputCls} />
|
|
88
|
+
</Field>
|
|
89
|
+
</div>
|
|
90
|
+
|
|
91
|
+
<div className="mt-3 border-t border-zinc-100 pt-4">
|
|
92
|
+
<div className="grid gap-3 sm:grid-cols-2">
|
|
93
|
+
<Field label="Tool name" required>
|
|
94
|
+
<input value={form.name} onChange={set("name")} className={inputCls} />
|
|
95
|
+
</Field>
|
|
96
|
+
<Field label="Category">
|
|
97
|
+
<select value={form.category} onChange={set("category")} className={inputCls}>
|
|
98
|
+
{categories.map((c) => (
|
|
99
|
+
<option key={c} value={c}>{c}</option>
|
|
100
|
+
))}
|
|
101
|
+
</select>
|
|
102
|
+
</Field>
|
|
103
|
+
</div>
|
|
104
|
+
<div className="mt-3">
|
|
105
|
+
<Field label="URL" required>
|
|
106
|
+
<input value={form.url} onChange={set("url")} placeholder="https://…" inputMode="url" className={inputCls} />
|
|
107
|
+
</Field>
|
|
108
|
+
</div>
|
|
109
|
+
<div className="mt-3">
|
|
110
|
+
<Field label="One-line tagline">
|
|
111
|
+
<input value={form.tagline} onChange={set("tagline")} placeholder="What it does, in a sentence." className={inputCls} />
|
|
112
|
+
</Field>
|
|
113
|
+
</div>
|
|
114
|
+
<div className="mt-3">
|
|
115
|
+
<Field label="Tags">
|
|
116
|
+
<input value={form.tags} onChange={set("tags")} placeholder="comma, separated, tags" className={inputCls} />
|
|
117
|
+
</Field>
|
|
118
|
+
</div>
|
|
119
|
+
<div className="mt-3">
|
|
120
|
+
<Field label="Anything else?">
|
|
121
|
+
<textarea value={form.description} onChange={set("description")} rows={3} className={inputCls + " resize-none py-2.5"} />
|
|
122
|
+
</Field>
|
|
123
|
+
</div>
|
|
124
|
+
</div>
|
|
125
|
+
|
|
126
|
+
{error ? <p className="mt-3 text-[13px] text-red-600">{error}</p> : null}
|
|
127
|
+
<button
|
|
128
|
+
type="submit"
|
|
129
|
+
disabled={status === "sending"}
|
|
130
|
+
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"
|
|
131
|
+
>
|
|
132
|
+
{status === "sending" ? "Submitting…" : "Submit for review"}
|
|
133
|
+
</button>
|
|
134
|
+
</form>
|
|
135
|
+
);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function Field({ label, required, children }: { label: string; required?: boolean; children: React.ReactNode }) {
|
|
139
|
+
return (
|
|
140
|
+
<label className="block">
|
|
141
|
+
<span className="mb-1.5 block text-[12.5px] font-medium text-zinc-600">
|
|
142
|
+
{label}
|
|
143
|
+
{required ? <span className="text-brand"> *</span> : null}
|
|
144
|
+
</span>
|
|
145
|
+
{children}
|
|
146
|
+
</label>
|
|
147
|
+
);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const inputCls =
|
|
151
|
+
"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";
|