@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,129 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import React, { useState } from "react";
|
|
4
|
+
import {
|
|
5
|
+
passwordLogin,
|
|
6
|
+
passwordRegister,
|
|
7
|
+
persistSession,
|
|
8
|
+
ApiError,
|
|
9
|
+
} from "@pylonsync/client";
|
|
10
|
+
|
|
11
|
+
// The owner's email/password form — one form, two modes. It calls the built-in
|
|
12
|
+
// auth API directly (`passwordLogin` / `passwordRegister` POST to
|
|
13
|
+
// `/api/auth/password/*`), then `persistSession` writes the freshly-minted
|
|
14
|
+
// token to local storage so the sync engine + `callFn` authenticate AS THE
|
|
15
|
+
// OWNER on the next load. This step matters here specifically: the landing page
|
|
16
|
+
// mints an anonymous guest session (for the live counter), and without
|
|
17
|
+
// persisting the real session that stale guest token would shadow the owner's
|
|
18
|
+
// — so the owner-only `waitlistStats` call would come back as a guest and get
|
|
19
|
+
// rejected. We then do a full navigation to /dashboard so the SSR runtime
|
|
20
|
+
// re-resolves auth from the HttpOnly cookie and renders server-side.
|
|
21
|
+
//
|
|
22
|
+
// A waitlist is single-tenant: there's no public signup funnel, just the owner
|
|
23
|
+
// creating their one account. Whoever signs in only sees data if their email
|
|
24
|
+
// matches PYLON_OWNER_EMAIL — enforced by the waitlistStats function.
|
|
25
|
+
export function AuthForm() {
|
|
26
|
+
const [mode, setMode] = useState<"login" | "signup">("login");
|
|
27
|
+
const [email, setEmail] = useState("");
|
|
28
|
+
const [password, setPassword] = useState("");
|
|
29
|
+
const [error, setError] = useState<string | null>(null);
|
|
30
|
+
const [pending, setPending] = useState(false);
|
|
31
|
+
|
|
32
|
+
async function onSubmit(e: React.FormEvent) {
|
|
33
|
+
e.preventDefault();
|
|
34
|
+
setError(null);
|
|
35
|
+
setPending(true);
|
|
36
|
+
try {
|
|
37
|
+
const session =
|
|
38
|
+
mode === "login"
|
|
39
|
+
? await passwordLogin({ email, password })
|
|
40
|
+
: await passwordRegister({ email, password });
|
|
41
|
+
// Make this session authoritative, replacing any anonymous guest token.
|
|
42
|
+
persistSession(session);
|
|
43
|
+
window.location.assign("/dashboard");
|
|
44
|
+
} catch (err) {
|
|
45
|
+
setError(messageFor(err));
|
|
46
|
+
setPending(false);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return (
|
|
51
|
+
<div className="space-y-5">
|
|
52
|
+
<form onSubmit={onSubmit} className="space-y-4">
|
|
53
|
+
<label className="block">
|
|
54
|
+
<span className="mb-1.5 block text-[13px] font-medium text-zinc-700">Email</span>
|
|
55
|
+
<input
|
|
56
|
+
type="email"
|
|
57
|
+
value={email}
|
|
58
|
+
onChange={(e) => setEmail(e.target.value)}
|
|
59
|
+
required
|
|
60
|
+
autoComplete="email"
|
|
61
|
+
placeholder="you@yourbusiness.com"
|
|
62
|
+
className="h-10 w-full rounded-lg border border-zinc-300 bg-white px-3 text-sm text-zinc-900 outline-none transition placeholder:text-zinc-400 focus:border-brand focus:ring-2 focus:ring-brand/20"
|
|
63
|
+
/>
|
|
64
|
+
</label>
|
|
65
|
+
<label className="block">
|
|
66
|
+
<span className="mb-1.5 block text-[13px] font-medium text-zinc-700">Password</span>
|
|
67
|
+
<input
|
|
68
|
+
type="password"
|
|
69
|
+
value={password}
|
|
70
|
+
onChange={(e) => setPassword(e.target.value)}
|
|
71
|
+
required
|
|
72
|
+
autoComplete={mode === "login" ? "current-password" : "new-password"}
|
|
73
|
+
placeholder={mode === "login" ? "Your password" : "At least 10 characters"}
|
|
74
|
+
className="h-10 w-full rounded-lg border border-zinc-300 bg-white px-3 text-sm text-zinc-900 outline-none transition placeholder:text-zinc-400 focus:border-brand focus:ring-2 focus:ring-brand/20"
|
|
75
|
+
/>
|
|
76
|
+
</label>
|
|
77
|
+
{error ? (
|
|
78
|
+
<p className="rounded-lg border border-red-200 bg-red-50 px-3 py-2 text-[13px] leading-snug text-red-700">
|
|
79
|
+
{error}
|
|
80
|
+
</p>
|
|
81
|
+
) : null}
|
|
82
|
+
<button
|
|
83
|
+
type="submit"
|
|
84
|
+
disabled={pending}
|
|
85
|
+
className="inline-flex h-10 w-full items-center justify-center rounded-lg bg-zinc-900 text-sm font-medium text-white transition-colors hover:bg-zinc-700 disabled:opacity-60"
|
|
86
|
+
>
|
|
87
|
+
{pending ? "…" : mode === "login" ? "Sign in" : "Create account"}
|
|
88
|
+
</button>
|
|
89
|
+
</form>
|
|
90
|
+
|
|
91
|
+
<p className="text-center text-[13px] text-zinc-500">
|
|
92
|
+
{mode === "login" ? "First time here?" : "Already have an account?"}{" "}
|
|
93
|
+
<button
|
|
94
|
+
type="button"
|
|
95
|
+
onClick={() => {
|
|
96
|
+
setMode(mode === "login" ? "signup" : "login");
|
|
97
|
+
setError(null);
|
|
98
|
+
}}
|
|
99
|
+
className="font-medium text-zinc-900 underline underline-offset-2"
|
|
100
|
+
>
|
|
101
|
+
{mode === "login" ? "Create the owner account" : "Sign in"}
|
|
102
|
+
</button>
|
|
103
|
+
</p>
|
|
104
|
+
</div>
|
|
105
|
+
);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Map the framework's auth error codes to friendly copy. `ApiError` carries a
|
|
109
|
+
// stable `.code` so you branch on the code, not the message.
|
|
110
|
+
function messageFor(err: unknown): string {
|
|
111
|
+
if (err instanceof ApiError) {
|
|
112
|
+
switch (err.code) {
|
|
113
|
+
case "INVALID_CREDENTIALS":
|
|
114
|
+
return "Wrong email or password.";
|
|
115
|
+
case "USER_EXISTS":
|
|
116
|
+
return "That email is already registered — sign in instead.";
|
|
117
|
+
case "WEAK_PASSWORD":
|
|
118
|
+
return "Pick a longer password — at least 10 characters.";
|
|
119
|
+
case "PWNED_PASSWORD":
|
|
120
|
+
return "That password has appeared in a known data breach. Choose a different one.";
|
|
121
|
+
case "RATE_LIMITED":
|
|
122
|
+
return "Too many attempts — try again in a minute.";
|
|
123
|
+
default:
|
|
124
|
+
return err.message;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
if (err instanceof Error) return err.message;
|
|
128
|
+
return "Something went wrong. Try again.";
|
|
129
|
+
}
|
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import React, { useEffect, useState } from "react";
|
|
4
|
+
import { db, callFn } from "@pylonsync/react";
|
|
5
|
+
import { useAuth } from "@pylonsync/client";
|
|
6
|
+
import { formatPrice, type OrderRow, type OwnerOrdersResult } from "@/lib/shop";
|
|
7
|
+
|
|
8
|
+
interface ProductRow {
|
|
9
|
+
id: string;
|
|
10
|
+
slug: string;
|
|
11
|
+
name: string;
|
|
12
|
+
stock: number;
|
|
13
|
+
priceCents: number;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// The owner's live dashboard. Stock rides the public Product entity
|
|
17
|
+
// (`db.useQuery`), so the stock table updates live as orders land; that same
|
|
18
|
+
// change also triggers a re-fetch of the owner-gated `ordersForOwner`, so new
|
|
19
|
+
// orders appear without a refresh. Customer PII only travels through that gated
|
|
20
|
+
// call.
|
|
21
|
+
export function ShopDashboard({ userEmail }: { userEmail: string }) {
|
|
22
|
+
const { data: products } = db.useQuery<ProductRow>("Product");
|
|
23
|
+
// A signature of stock state — changes whenever any product's stock moves,
|
|
24
|
+
// which is exactly when a new order landed or one was cancelled.
|
|
25
|
+
const liveKey = products.reduce((s, p) => s + p.stock, 0) + ":" + products.length;
|
|
26
|
+
|
|
27
|
+
const [orders, setOrders] = useState<OrderRow[] | null>(null);
|
|
28
|
+
const [denied, setDenied] = useState(false);
|
|
29
|
+
const [error, setError] = useState<string | null>(null);
|
|
30
|
+
const [busyId, setBusyId] = useState<string | null>(null);
|
|
31
|
+
|
|
32
|
+
async function load() {
|
|
33
|
+
try {
|
|
34
|
+
const r = await callFn<OwnerOrdersResult>("ordersForOwner", {});
|
|
35
|
+
if (!r.authorized) setDenied(true);
|
|
36
|
+
else {
|
|
37
|
+
setOrders(r.orders);
|
|
38
|
+
setDenied(false);
|
|
39
|
+
setError(null);
|
|
40
|
+
}
|
|
41
|
+
} catch (e) {
|
|
42
|
+
setError(e instanceof Error ? e.message : String(e));
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
useEffect(() => {
|
|
47
|
+
void load();
|
|
48
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
49
|
+
}, [liveKey]);
|
|
50
|
+
|
|
51
|
+
async function orderAction(id: string, fn: "fulfillOrder" | "cancelOrder") {
|
|
52
|
+
setBusyId(id);
|
|
53
|
+
try {
|
|
54
|
+
await callFn(fn, { orderId: id });
|
|
55
|
+
await load();
|
|
56
|
+
} finally {
|
|
57
|
+
setBusyId(null);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
async function restock(slug: string) {
|
|
62
|
+
setBusyId(slug);
|
|
63
|
+
try {
|
|
64
|
+
await callFn("restockProduct", { slug, add: 10 });
|
|
65
|
+
} finally {
|
|
66
|
+
setBusyId(null);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (denied) return <OwnerOnly email={userEmail} />;
|
|
71
|
+
if (error) {
|
|
72
|
+
return <div className="rounded-xl border border-red-200 bg-red-50 px-5 py-4 text-sm text-red-700">{error}</div>;
|
|
73
|
+
}
|
|
74
|
+
if (!orders) return <Skeleton />;
|
|
75
|
+
|
|
76
|
+
const active = orders.filter((o) => o.status !== "cancelled");
|
|
77
|
+
// A sale = paid / reserved / fulfilled. "pending" is an unpaid Stripe checkout
|
|
78
|
+
// still in flight (stock held), so it doesn't count toward sold/revenue yet.
|
|
79
|
+
const sales = active.filter((o) => o.status !== "pending");
|
|
80
|
+
const toFulfill = sales.filter((o) => o.status === "paid" || o.status === "reserved").length;
|
|
81
|
+
const unitsSold = sales.reduce((s, o) => s + o.qty, 0);
|
|
82
|
+
const revenue = sales.reduce((s, o) => s + o.qty * o.unitPriceCents, 0);
|
|
83
|
+
|
|
84
|
+
return (
|
|
85
|
+
<div className="space-y-8">
|
|
86
|
+
<div>
|
|
87
|
+
<h1 className="text-xl font-semibold tracking-tight">Orders</h1>
|
|
88
|
+
<p className="mt-1 text-sm text-zinc-500">
|
|
89
|
+
Live — orders land here the moment they happen; cancelling returns the units to stock on
|
|
90
|
+
the site instantly.
|
|
91
|
+
</p>
|
|
92
|
+
</div>
|
|
93
|
+
|
|
94
|
+
<div className="grid gap-4 sm:grid-cols-3">
|
|
95
|
+
<Stat label="To fulfill" value={String(toFulfill)} />
|
|
96
|
+
<Stat label="Units sold" value={String(unitsSold)} />
|
|
97
|
+
<Stat label="Revenue" value={formatPrice(revenue)} />
|
|
98
|
+
</div>
|
|
99
|
+
|
|
100
|
+
{/* Stock */}
|
|
101
|
+
<div className="rounded-xl border border-zinc-200 bg-white">
|
|
102
|
+
<div className="border-b border-zinc-100 px-4 py-3 text-sm font-semibold text-zinc-900">Stock</div>
|
|
103
|
+
<ul className="divide-y divide-zinc-100">
|
|
104
|
+
{products
|
|
105
|
+
.slice()
|
|
106
|
+
.sort((a, b) => a.name.localeCompare(b.name))
|
|
107
|
+
.map((p) => (
|
|
108
|
+
<li key={p.id} className="flex items-center justify-between gap-3 px-4 py-2.5">
|
|
109
|
+
<span className="truncate text-sm text-zinc-800">{p.name}</span>
|
|
110
|
+
<div className="flex items-center gap-3">
|
|
111
|
+
<span
|
|
112
|
+
className={
|
|
113
|
+
"text-[13px] font-medium tabular-nums " +
|
|
114
|
+
(p.stock <= 0 ? "text-zinc-400" : p.stock <= 3 ? "text-red-600" : "text-zinc-700")
|
|
115
|
+
}
|
|
116
|
+
>
|
|
117
|
+
{p.stock <= 0 ? "Sold out" : `${p.stock} in stock`}
|
|
118
|
+
</span>
|
|
119
|
+
<button
|
|
120
|
+
type="button"
|
|
121
|
+
disabled={busyId === p.slug}
|
|
122
|
+
onClick={() => restock(p.slug)}
|
|
123
|
+
className="rounded-md border border-zinc-300 px-2.5 py-1 text-[12px] font-medium text-zinc-600 transition-colors hover:bg-zinc-50 disabled:opacity-50"
|
|
124
|
+
>
|
|
125
|
+
+10
|
|
126
|
+
</button>
|
|
127
|
+
</div>
|
|
128
|
+
</li>
|
|
129
|
+
))}
|
|
130
|
+
</ul>
|
|
131
|
+
</div>
|
|
132
|
+
|
|
133
|
+
{/* Orders */}
|
|
134
|
+
<div className="rounded-xl border border-zinc-200 bg-white">
|
|
135
|
+
<div className="border-b border-zinc-100 px-4 py-3 text-sm font-semibold text-zinc-900">
|
|
136
|
+
Orders <span className="font-normal text-zinc-400">({active.length})</span>
|
|
137
|
+
</div>
|
|
138
|
+
{active.length === 0 ? (
|
|
139
|
+
<p className="p-8 text-center text-sm text-zinc-500">No orders yet — share your shop.</p>
|
|
140
|
+
) : (
|
|
141
|
+
<ul className="divide-y divide-zinc-100">
|
|
142
|
+
{active.map((o) => (
|
|
143
|
+
<li key={o.id} className="flex flex-wrap items-center gap-x-4 gap-y-2 px-4 py-3">
|
|
144
|
+
<div className="min-w-0 flex-1">
|
|
145
|
+
<div className="flex items-center gap-2">
|
|
146
|
+
<span className="text-[14px] font-medium text-zinc-900">
|
|
147
|
+
{o.qty}× {o.productName}
|
|
148
|
+
</span>
|
|
149
|
+
<StatusBadge status={o.status} />
|
|
150
|
+
</div>
|
|
151
|
+
<div className="truncate text-[12.5px] text-zinc-500">
|
|
152
|
+
{o.customerName} · {o.customerEmail} · {formatPrice(o.qty * o.unitPriceCents)}
|
|
153
|
+
</div>
|
|
154
|
+
</div>
|
|
155
|
+
<div className="flex items-center gap-2">
|
|
156
|
+
{o.status === "reserved" || o.status === "paid" ? (
|
|
157
|
+
<button
|
|
158
|
+
type="button"
|
|
159
|
+
disabled={busyId === o.id}
|
|
160
|
+
onClick={() => orderAction(o.id, "fulfillOrder")}
|
|
161
|
+
className="rounded-md bg-zinc-900 px-3 py-1.5 text-[12.5px] font-medium text-white transition-colors hover:bg-zinc-700 disabled:opacity-50"
|
|
162
|
+
>
|
|
163
|
+
{busyId === o.id ? "…" : "Mark fulfilled"}
|
|
164
|
+
</button>
|
|
165
|
+
) : null}
|
|
166
|
+
<button
|
|
167
|
+
type="button"
|
|
168
|
+
disabled={busyId === o.id}
|
|
169
|
+
onClick={() => orderAction(o.id, "cancelOrder")}
|
|
170
|
+
className="rounded-md border border-zinc-300 px-3 py-1.5 text-[12.5px] font-medium text-zinc-600 transition-colors hover:border-red-300 hover:text-red-600 disabled:opacity-50"
|
|
171
|
+
>
|
|
172
|
+
Cancel
|
|
173
|
+
</button>
|
|
174
|
+
</div>
|
|
175
|
+
</li>
|
|
176
|
+
))}
|
|
177
|
+
</ul>
|
|
178
|
+
)}
|
|
179
|
+
</div>
|
|
180
|
+
</div>
|
|
181
|
+
);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function Stat({ label, value }: { label: string; value: string }) {
|
|
185
|
+
return (
|
|
186
|
+
<div className="rounded-xl border border-zinc-200 bg-white p-4">
|
|
187
|
+
<div className="text-[11px] font-medium uppercase tracking-wide text-zinc-400">{label}</div>
|
|
188
|
+
<div className="mt-1 text-2xl font-semibold tabular-nums text-zinc-900">{value}</div>
|
|
189
|
+
</div>
|
|
190
|
+
);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function StatusBadge({ status }: { status: string }) {
|
|
194
|
+
const tone =
|
|
195
|
+
status === "fulfilled"
|
|
196
|
+
? "bg-green-50 text-green-700"
|
|
197
|
+
: status === "paid"
|
|
198
|
+
? "bg-emerald-50 text-emerald-700"
|
|
199
|
+
: status === "pending"
|
|
200
|
+
? "bg-zinc-100 text-zinc-500"
|
|
201
|
+
: status === "cancelled"
|
|
202
|
+
? "bg-zinc-100 text-zinc-400"
|
|
203
|
+
: "bg-amber-50 text-amber-700"; // reserved
|
|
204
|
+
const label = status === "pending" ? "awaiting payment" : status;
|
|
205
|
+
return <span className={"rounded-full px-2 py-0.5 text-[10px] font-medium capitalize " + tone}>{label}</span>;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function OwnerOnly({ email }: { email: string }) {
|
|
209
|
+
return (
|
|
210
|
+
<div className="rounded-xl border border-dashed border-zinc-300 px-6 py-12 text-center">
|
|
211
|
+
<h1 className="text-lg font-semibold">This dashboard is owner-only</h1>
|
|
212
|
+
<p className="mx-auto mt-2 max-w-md text-sm text-zinc-500">
|
|
213
|
+
You're signed in as <span className="font-medium text-zinc-700">{email || "this account"}</span>.
|
|
214
|
+
Only the shop owner can see orders. Set{" "}
|
|
215
|
+
<code className="rounded bg-zinc-100 px-1.5 py-0.5 text-[12px]">PYLON_OWNER_EMAIL={email || "you@yourshop.com"}</code>{" "}
|
|
216
|
+
in your <code className="rounded bg-zinc-100 px-1.5 py-0.5 text-[12px]">.env</code>, restart, and reload —
|
|
217
|
+
or sign in with the owner account.
|
|
218
|
+
</p>
|
|
219
|
+
</div>
|
|
220
|
+
);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
export function UserMenu({ email }: { email: string }) {
|
|
224
|
+
const { signOut } = useAuth();
|
|
225
|
+
const initial = (email.trim()[0] || "?").toUpperCase();
|
|
226
|
+
async function onSignOut() {
|
|
227
|
+
await signOut();
|
|
228
|
+
window.location.assign("/");
|
|
229
|
+
}
|
|
230
|
+
return (
|
|
231
|
+
<details className="group relative">
|
|
232
|
+
<summary className="flex size-8 cursor-pointer select-none list-none items-center justify-center rounded-full bg-zinc-900 text-[12px] font-semibold text-white marker:hidden [&::-webkit-details-marker]:hidden">
|
|
233
|
+
{initial}
|
|
234
|
+
</summary>
|
|
235
|
+
<div className="absolute right-0 top-full z-40 mt-2 w-56 overflow-hidden rounded-xl border border-zinc-200 bg-white py-1 shadow-[0_16px_48px_-16px_rgba(0,0,0,0.25)]">
|
|
236
|
+
<div className="border-b border-zinc-100 px-3 py-2">
|
|
237
|
+
<div className="truncate text-[13px] font-medium text-zinc-900">{email || "Signed in"}</div>
|
|
238
|
+
</div>
|
|
239
|
+
<button
|
|
240
|
+
type="button"
|
|
241
|
+
onClick={onSignOut}
|
|
242
|
+
className="flex w-full items-center px-3 py-2 text-left text-[13px] text-zinc-700 transition-colors hover:bg-zinc-50"
|
|
243
|
+
>
|
|
244
|
+
Sign out
|
|
245
|
+
</button>
|
|
246
|
+
</div>
|
|
247
|
+
</details>
|
|
248
|
+
);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
function Skeleton() {
|
|
252
|
+
return (
|
|
253
|
+
<div className="space-y-8">
|
|
254
|
+
<div className="h-6 w-28 animate-pulse rounded bg-zinc-100" />
|
|
255
|
+
<div className="grid gap-4 sm:grid-cols-3">
|
|
256
|
+
{[0, 1, 2].map((i) => (
|
|
257
|
+
<div key={i} className="h-20 animate-pulse rounded-xl bg-zinc-100" />
|
|
258
|
+
))}
|
|
259
|
+
</div>
|
|
260
|
+
<div className="h-40 animate-pulse rounded-xl bg-zinc-100" />
|
|
261
|
+
<div className="h-48 animate-pulse rounded-xl bg-zinc-100" />
|
|
262
|
+
</div>
|
|
263
|
+
);
|
|
264
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import React, { use } from "react";
|
|
2
|
+
import { Link, type Metadata, type PageProps } from "@pylonsync/react";
|
|
3
|
+
import { siteConfig } from "@/lib/site.config";
|
|
4
|
+
import { UserMenu, ShopDashboard } from "./dashboard-client";
|
|
5
|
+
|
|
6
|
+
export const metadata: Metadata = {
|
|
7
|
+
title: `Dashboard — ${siteConfig.brand.name}`,
|
|
8
|
+
robots: "noindex",
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
// `app/dashboard/page.tsx` → `/dashboard`. Server-side auth gate; the OWNER gate
|
|
12
|
+
// (only PYLON_OWNER_EMAIL sees orders + customer PII) lives in the
|
|
13
|
+
// `ordersForOwner` function via `ctx.env`. Reading `auth` opts the render out of
|
|
14
|
+
// caching — correct, the dashboard is private + noindex.
|
|
15
|
+
export default function DashboardPage({ auth, response, serverData }: PageProps) {
|
|
16
|
+
// Anonymous visitors and guest sessions (guest_… ids) get bounced to login —
|
|
17
|
+
// the dashboard is for the real, signed-in owner only.
|
|
18
|
+
if (!auth.user_id || auth.user_id.startsWith("guest_")) {
|
|
19
|
+
response.redirect("/login");
|
|
20
|
+
return null;
|
|
21
|
+
}
|
|
22
|
+
const me = use(serverData.get<{ email?: string }>("User", auth.user_id));
|
|
23
|
+
const email = me?.email ?? "";
|
|
24
|
+
|
|
25
|
+
return (
|
|
26
|
+
<Shell email={email}>
|
|
27
|
+
<ShopDashboard userEmail={email} />
|
|
28
|
+
</Shell>
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function Shell({ email, children }: { email: string; children: React.ReactNode }) {
|
|
33
|
+
const { brand } = siteConfig;
|
|
34
|
+
return (
|
|
35
|
+
<div className="flex min-h-screen flex-col bg-white text-zinc-900">
|
|
36
|
+
<header className="border-b border-zinc-200">
|
|
37
|
+
<div className="mx-auto flex h-14 max-w-4xl items-center justify-between px-6">
|
|
38
|
+
<div className="flex items-center gap-2">
|
|
39
|
+
<span className="flex size-6 items-center justify-center rounded-[7px] bg-zinc-900 text-[13px] font-bold text-white">
|
|
40
|
+
{brand.letter}
|
|
41
|
+
</span>
|
|
42
|
+
<span className="text-[15px] font-semibold tracking-tight">
|
|
43
|
+
{brand.name} <span className="text-zinc-400">/ orders</span>
|
|
44
|
+
</span>
|
|
45
|
+
</div>
|
|
46
|
+
<div className="flex items-center gap-4">
|
|
47
|
+
<Link href="/" className="text-[13px] text-zinc-500 transition-colors hover:text-zinc-900">
|
|
48
|
+
View site ↗
|
|
49
|
+
</Link>
|
|
50
|
+
<UserMenu email={email} />
|
|
51
|
+
</div>
|
|
52
|
+
</div>
|
|
53
|
+
</header>
|
|
54
|
+
<main className="flex-1">
|
|
55
|
+
<div className="mx-auto max-w-4xl px-6 py-8">{children}</div>
|
|
56
|
+
</main>
|
|
57
|
+
</div>
|
|
58
|
+
);
|
|
59
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { type ErrorBoundaryProps } from "@pylonsync/react";
|
|
3
|
+
|
|
4
|
+
// `app/error.tsx` → the error boundary for this segment. Hydrated + interactive:
|
|
5
|
+
// `reset()` re-attempts the route. The thrown error reaches the client as
|
|
6
|
+
// `{ message, digest }` only — the stack stays in the dev overlay / server logs.
|
|
7
|
+
export default function Error({ error, reset }: ErrorBoundaryProps) {
|
|
8
|
+
return (
|
|
9
|
+
<div className="mx-auto flex min-h-[60vh] max-w-3xl flex-col items-center justify-center px-6 text-center">
|
|
10
|
+
<h1 className="text-2xl font-semibold tracking-tight">Something went wrong</h1>
|
|
11
|
+
<p className="mt-2 text-zinc-500">{error.message}</p>
|
|
12
|
+
{error.digest ? (
|
|
13
|
+
<p className="mt-1 text-xs text-zinc-400">
|
|
14
|
+
Reference: <code>{error.digest}</code>
|
|
15
|
+
</p>
|
|
16
|
+
) : null}
|
|
17
|
+
<button
|
|
18
|
+
type="button"
|
|
19
|
+
onClick={reset}
|
|
20
|
+
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"
|
|
21
|
+
>
|
|
22
|
+
Try again
|
|
23
|
+
</button>
|
|
24
|
+
</div>
|
|
25
|
+
);
|
|
26
|
+
}
|
|
@@ -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
|
+
}
|