@pylonsync/create-pylon 0.3.274 → 0.3.276
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/create-pylon.js +80 -0
- package/package.json +1 -1
- package/templates/ARCHETYPES.md +339 -0
- package/templates/agency/.env.example +12 -0
- package/templates/agency/AGENTS.md +61 -0
- package/templates/agency/README.md +90 -0
- package/templates/agency/app/auth-form.tsx +129 -0
- package/templates/agency/app/contact-form.tsx +258 -0
- package/templates/agency/app/dashboard/dashboard-client.tsx +1440 -0
- package/templates/agency/app/dashboard/page.tsx +70 -0
- package/templates/agency/app/error.tsx +26 -0
- package/templates/agency/app/globals.css +148 -0
- package/templates/agency/app/layout.tsx +174 -0
- package/templates/agency/app/login/page.tsx +39 -0
- package/templates/agency/app/not-found.tsx +19 -0
- package/templates/agency/app/page.tsx +249 -0
- package/templates/agency/app/robots.ts +12 -0
- package/templates/agency/app/seeder.tsx +26 -0
- package/templates/agency/app/sitemap.ts +9 -0
- package/templates/agency/app/work/[slug]/page.tsx +182 -0
- package/templates/agency/app/work/page.tsx +83 -0
- package/templates/agency/app.ts +284 -0
- package/templates/agency/components/marketing.tsx +187 -0
- package/templates/agency/components/section-scroller.tsx +35 -0
- package/templates/agency/components/ui/button.tsx +56 -0
- package/templates/agency/components/ui/card.tsx +90 -0
- package/templates/agency/components.json +20 -0
- package/templates/agency/functions/bookInquiry.ts +42 -0
- package/templates/agency/functions/clientsForOwner.ts +27 -0
- package/templates/agency/functions/declineInquiry.ts +41 -0
- package/templates/agency/functions/deleteClient.ts +27 -0
- package/templates/agency/functions/deleteInvoice.ts +19 -0
- package/templates/agency/functions/deleteProject.ts +20 -0
- package/templates/agency/functions/inquiriesForOwner.ts +31 -0
- package/templates/agency/functions/invoicesForOwner.ts +27 -0
- package/templates/agency/functions/seedCapacity.ts +26 -0
- package/templates/agency/functions/seedProjects.ts +41 -0
- package/templates/agency/functions/seedStudioBackoffice.ts +74 -0
- package/templates/agency/functions/setCapacity.ts +32 -0
- package/templates/agency/functions/setInvoiceStatus.ts +27 -0
- package/templates/agency/functions/setProjectFlags.ts +35 -0
- package/templates/agency/functions/submitInquiry.ts +55 -0
- package/templates/agency/functions/upsertClient.ts +73 -0
- package/templates/agency/functions/upsertInvoice.ts +113 -0
- package/templates/agency/functions/upsertProject.ts +97 -0
- package/templates/agency/gitignore +10 -0
- package/templates/agency/lib/agency.ts +189 -0
- package/templates/agency/lib/invoice-pdf.tsx +174 -0
- package/templates/agency/lib/owner.ts +26 -0
- package/templates/agency/lib/site.config.ts +418 -0
- package/templates/agency/lib/utils.ts +10 -0
- package/templates/agency/package.json +35 -0
- package/templates/agency/tsconfig.json +18 -0
- package/templates/ai-chat/.env.example +33 -0
- package/templates/ai-chat/AGENTS.md +61 -0
- package/templates/ai-chat/README.md +99 -0
- package/templates/ai-chat/app/auth-form.tsx +124 -0
- package/templates/ai-chat/app/chat-client.tsx +727 -0
- package/templates/ai-chat/app/error.tsx +26 -0
- package/templates/ai-chat/app/globals.css +148 -0
- package/templates/ai-chat/app/layout.tsx +75 -0
- package/templates/ai-chat/app/login/page.tsx +39 -0
- package/templates/ai-chat/app/not-found.tsx +19 -0
- package/templates/ai-chat/app/page.tsx +23 -0
- package/templates/ai-chat/app.ts +121 -0
- package/templates/ai-chat/components.json +20 -0
- package/templates/ai-chat/functions/deleteConversation.ts +33 -0
- package/templates/ai-chat/gitignore +10 -0
- package/templates/ai-chat/lib/site.config.ts +103 -0
- package/templates/ai-chat/lib/utils.ts +10 -0
- package/templates/ai-chat/package.json +34 -0
- package/templates/ai-chat/tsconfig.json +18 -0
- package/templates/ai-studio/.env.example +19 -0
- package/templates/ai-studio/AGENTS.md +61 -0
- package/templates/ai-studio/README.md +83 -0
- package/templates/ai-studio/app/auth-form.tsx +124 -0
- package/templates/ai-studio/app/error.tsx +26 -0
- package/templates/ai-studio/app/globals.css +148 -0
- package/templates/ai-studio/app/layout.tsx +75 -0
- package/templates/ai-studio/app/login/page.tsx +39 -0
- package/templates/ai-studio/app/not-found.tsx +19 -0
- package/templates/ai-studio/app/page.tsx +34 -0
- package/templates/ai-studio/app/studio-client.tsx +357 -0
- package/templates/ai-studio/app.ts +108 -0
- package/templates/ai-studio/components.json +20 -0
- package/templates/ai-studio/functions/_getGeneration.ts +25 -0
- package/templates/ai-studio/functions/_updateGeneration.ts +37 -0
- package/templates/ai-studio/functions/generate.ts +42 -0
- package/templates/ai-studio/functions/pollGeneration.ts +134 -0
- package/templates/ai-studio/gitignore +10 -0
- package/templates/ai-studio/lib/site.config.ts +80 -0
- package/templates/ai-studio/lib/studio.ts +52 -0
- package/templates/ai-studio/lib/utils.ts +10 -0
- package/templates/ai-studio/package.json +34 -0
- package/templates/ai-studio/tsconfig.json +18 -0
- package/templates/creator/.env.example +12 -0
- package/templates/creator/AGENTS.md +61 -0
- package/templates/creator/README.md +67 -0
- package/templates/creator/app/auth-form.tsx +129 -0
- package/templates/creator/app/dashboard/dashboard-client.tsx +297 -0
- package/templates/creator/app/dashboard/page.tsx +70 -0
- package/templates/creator/app/error.tsx +26 -0
- package/templates/creator/app/globals.css +148 -0
- package/templates/creator/app/layout.tsx +160 -0
- package/templates/creator/app/login/page.tsx +39 -0
- package/templates/creator/app/newsletter-signup.tsx +162 -0
- package/templates/creator/app/not-found.tsx +19 -0
- package/templates/creator/app/page.tsx +160 -0
- package/templates/creator/app/robots.ts +12 -0
- package/templates/creator/app/sitemap.ts +9 -0
- package/templates/creator/app.ts +134 -0
- package/templates/creator/components/marketing.tsx +148 -0
- package/templates/creator/components/section-scroller.tsx +35 -0
- package/templates/creator/components/ui/button.tsx +56 -0
- package/templates/creator/components/ui/card.tsx +90 -0
- package/templates/creator/components.json +20 -0
- package/templates/creator/functions/subscribe.ts +82 -0
- package/templates/creator/functions/subscriberStats.ts +75 -0
- package/templates/creator/gitignore +10 -0
- package/templates/creator/lib/owner.ts +26 -0
- package/templates/creator/lib/site.config.ts +173 -0
- package/templates/creator/lib/stats.ts +30 -0
- package/templates/creator/lib/utils.ts +10 -0
- package/templates/creator/package.json +34 -0
- package/templates/creator/tsconfig.json +18 -0
- package/templates/default/app/layout.tsx +26 -27
- package/templates/default/app/page.tsx +90 -274
- package/templates/default/lib/products.ts +9 -122
- package/templates/default/lib/site.config.ts +739 -0
- package/templates/default/lib/site.ts +14 -261
- package/templates/directory/.env.example +12 -0
- package/templates/directory/AGENTS.md +61 -0
- package/templates/directory/README.md +80 -0
- package/templates/directory/app/auth-form.tsx +129 -0
- package/templates/directory/app/dashboard/dashboard-client.tsx +205 -0
- package/templates/directory/app/dashboard/page.tsx +70 -0
- package/templates/directory/app/directory-browse.tsx +328 -0
- package/templates/directory/app/error.tsx +26 -0
- package/templates/directory/app/globals.css +148 -0
- package/templates/directory/app/layout.tsx +171 -0
- package/templates/directory/app/login/page.tsx +39 -0
- package/templates/directory/app/not-found.tsx +19 -0
- package/templates/directory/app/page.tsx +50 -0
- package/templates/directory/app/robots.ts +12 -0
- package/templates/directory/app/sitemap.ts +9 -0
- package/templates/directory/app/submit/page.tsx +30 -0
- package/templates/directory/app/submit-form.tsx +151 -0
- package/templates/directory/app.ts +146 -0
- package/templates/directory/components/marketing.tsx +148 -0
- package/templates/directory/components/section-scroller.tsx +35 -0
- package/templates/directory/components/ui/button.tsx +56 -0
- package/templates/directory/components/ui/card.tsx +90 -0
- package/templates/directory/components.json +20 -0
- package/templates/directory/functions/approveSubmission.ts +45 -0
- package/templates/directory/functions/rejectSubmission.ts +20 -0
- package/templates/directory/functions/seedListings.ts +33 -0
- package/templates/directory/functions/submissionsForOwner.ts +29 -0
- package/templates/directory/functions/submitListing.ts +63 -0
- package/templates/directory/functions/upvote.ts +24 -0
- package/templates/directory/gitignore +10 -0
- package/templates/directory/lib/directory.ts +45 -0
- package/templates/directory/lib/owner.ts +26 -0
- package/templates/directory/lib/site.config.ts +130 -0
- package/templates/directory/lib/utils.ts +10 -0
- package/templates/directory/package.json +34 -0
- package/templates/directory/tsconfig.json +18 -0
- package/templates/local-service/.env.example +12 -0
- package/templates/local-service/AGENTS.md +61 -0
- package/templates/local-service/README.md +82 -0
- package/templates/local-service/app/auth-form.tsx +129 -0
- package/templates/local-service/app/booking-widget.tsx +399 -0
- package/templates/local-service/app/dashboard/dashboard-client.tsx +304 -0
- package/templates/local-service/app/dashboard/page.tsx +63 -0
- package/templates/local-service/app/error.tsx +26 -0
- package/templates/local-service/app/globals.css +148 -0
- package/templates/local-service/app/layout.tsx +151 -0
- package/templates/local-service/app/login/page.tsx +39 -0
- package/templates/local-service/app/not-found.tsx +19 -0
- package/templates/local-service/app/page.tsx +233 -0
- package/templates/local-service/app/robots.ts +12 -0
- package/templates/local-service/app/sitemap.ts +9 -0
- package/templates/local-service/app.ts +131 -0
- package/templates/local-service/components/marketing.tsx +162 -0
- package/templates/local-service/components/section-scroller.tsx +35 -0
- package/templates/local-service/components/ui/button.tsx +56 -0
- package/templates/local-service/components/ui/card.tsx +90 -0
- package/templates/local-service/components.json +20 -0
- package/templates/local-service/functions/bookingsForOwner.ts +30 -0
- package/templates/local-service/functions/cancelBooking.ts +27 -0
- package/templates/local-service/functions/confirmBooking.ts +18 -0
- package/templates/local-service/functions/createBooking.ts +98 -0
- package/templates/local-service/gitignore +10 -0
- package/templates/local-service/lib/booking.ts +24 -0
- package/templates/local-service/lib/owner.ts +26 -0
- package/templates/local-service/lib/site.config.ts +232 -0
- package/templates/local-service/lib/slots.ts +97 -0
- package/templates/local-service/lib/utils.ts +10 -0
- package/templates/local-service/package.json +34 -0
- package/templates/local-service/tsconfig.json +18 -0
- package/templates/marketplace/.env.example +9 -0
- package/templates/marketplace/AGENTS.md +61 -0
- package/templates/marketplace/README.md +78 -0
- package/templates/marketplace/app/_components/CategoryIcon.tsx +40 -0
- package/templates/marketplace/app/error.tsx +26 -0
- package/templates/marketplace/app/globals.css +64 -0
- package/templates/marketplace/app/layout.tsx +60 -0
- package/templates/marketplace/app/listing/[id]/page.tsx +163 -0
- package/templates/marketplace/app/me/page.tsx +15 -0
- package/templates/marketplace/app/not-found.tsx +20 -0
- package/templates/marketplace/app/page.tsx +159 -0
- package/templates/marketplace/app/robots.ts +12 -0
- package/templates/marketplace/app/sell/page.tsx +26 -0
- package/templates/marketplace/app/sitemap.ts +14 -0
- package/templates/marketplace/app.ts +190 -0
- package/templates/marketplace/client/AuthNav.tsx +46 -0
- package/templates/marketplace/client/LiveTicker.tsx +104 -0
- package/templates/marketplace/client/LoginCard.tsx +130 -0
- package/templates/marketplace/client/MarketProvider.tsx +148 -0
- package/templates/marketplace/client/MyMarket.tsx +180 -0
- package/templates/marketplace/client/OfferPanel.tsx +355 -0
- package/templates/marketplace/client/SeedOnEmpty.tsx +26 -0
- package/templates/marketplace/client/SellForm.tsx +160 -0
- package/templates/marketplace/client/WatchButton.tsx +88 -0
- package/templates/marketplace/client/market.ts +341 -0
- package/templates/marketplace/functions/buyNow.ts +78 -0
- package/templates/marketplace/functions/makeOffer.ts +65 -0
- package/templates/marketplace/functions/respondToOffer.ts +62 -0
- package/templates/marketplace/functions/seedMarket.ts +90 -0
- package/templates/marketplace/gitignore +10 -0
- package/templates/marketplace/package.json +35 -0
- package/templates/marketplace/tsconfig.json +14 -0
- package/templates/marketplace/ui/badge.tsx +30 -0
- package/templates/marketplace/ui/button.tsx +49 -0
- package/templates/marketplace/ui/card.tsx +48 -0
- package/templates/marketplace/ui/input.tsx +17 -0
- package/templates/marketplace/ui/label.tsx +18 -0
- package/templates/marketplace/ui/textarea.tsx +17 -0
- package/templates/marketplace/ui/tokens.css +32 -0
- package/templates/marketplace/ui/utils.ts +6 -0
- package/templates/restaurant/.env.example +12 -0
- package/templates/restaurant/AGENTS.md +61 -0
- package/templates/restaurant/README.md +77 -0
- package/templates/restaurant/app/auth-form.tsx +129 -0
- package/templates/restaurant/app/dashboard/dashboard-client.tsx +263 -0
- package/templates/restaurant/app/dashboard/page.tsx +59 -0
- package/templates/restaurant/app/error.tsx +26 -0
- package/templates/restaurant/app/globals.css +148 -0
- package/templates/restaurant/app/layout.tsx +151 -0
- package/templates/restaurant/app/login/page.tsx +39 -0
- package/templates/restaurant/app/not-found.tsx +19 -0
- package/templates/restaurant/app/page.tsx +194 -0
- package/templates/restaurant/app/reservation-widget.tsx +359 -0
- package/templates/restaurant/app/robots.ts +12 -0
- package/templates/restaurant/app/sitemap.ts +9 -0
- package/templates/restaurant/app.ts +115 -0
- package/templates/restaurant/components/marketing.tsx +162 -0
- package/templates/restaurant/components/section-scroller.tsx +35 -0
- package/templates/restaurant/components/ui/button.tsx +56 -0
- package/templates/restaurant/components/ui/card.tsx +90 -0
- package/templates/restaurant/components.json +20 -0
- package/templates/restaurant/functions/cancelReservation.ts +26 -0
- package/templates/restaurant/functions/confirmReservation.ts +17 -0
- package/templates/restaurant/functions/createReservation.ts +92 -0
- package/templates/restaurant/functions/reservationsForOwner.ts +28 -0
- package/templates/restaurant/gitignore +10 -0
- package/templates/restaurant/lib/owner.ts +26 -0
- package/templates/restaurant/lib/reservation.ts +22 -0
- package/templates/restaurant/lib/site.config.ts +218 -0
- package/templates/restaurant/lib/slots.ts +55 -0
- package/templates/restaurant/lib/utils.ts +10 -0
- package/templates/restaurant/package.json +34 -0
- package/templates/restaurant/tsconfig.json +18 -0
- package/templates/shop/.env.example +32 -0
- package/templates/shop/AGENTS.md +61 -0
- package/templates/shop/README.md +102 -0
- package/templates/shop/app/auth-form.tsx +129 -0
- package/templates/shop/app/dashboard/dashboard-client.tsx +264 -0
- package/templates/shop/app/dashboard/page.tsx +59 -0
- package/templates/shop/app/error.tsx +26 -0
- package/templates/shop/app/globals.css +148 -0
- package/templates/shop/app/layout.tsx +160 -0
- package/templates/shop/app/login/page.tsx +39 -0
- package/templates/shop/app/not-found.tsx +19 -0
- package/templates/shop/app/page.tsx +95 -0
- package/templates/shop/app/robots.ts +12 -0
- package/templates/shop/app/shop-client.tsx +436 -0
- package/templates/shop/app/sitemap.ts +9 -0
- package/templates/shop/app/success/page.tsx +33 -0
- package/templates/shop/app.ts +134 -0
- package/templates/shop/components/marketing.tsx +96 -0
- package/templates/shop/components/section-scroller.tsx +35 -0
- package/templates/shop/components/ui/button.tsx +56 -0
- package/templates/shop/components/ui/card.tsx +90 -0
- package/templates/shop/components.json +20 -0
- package/templates/shop/functions/cancelOrder.ts +33 -0
- package/templates/shop/functions/checkout.ts +130 -0
- package/templates/shop/functions/fulfillOrder.ts +17 -0
- package/templates/shop/functions/markGroupPaid.ts +26 -0
- package/templates/shop/functions/ordersForOwner.ts +28 -0
- package/templates/shop/functions/releaseGroup.ts +36 -0
- package/templates/shop/functions/reserveCart.ts +87 -0
- package/templates/shop/functions/restockProduct.ts +23 -0
- package/templates/shop/functions/seedProducts.ts +30 -0
- package/templates/shop/functions/stripeWebhook.ts +72 -0
- package/templates/shop/gitignore +10 -0
- package/templates/shop/lib/owner.ts +26 -0
- package/templates/shop/lib/shop.ts +45 -0
- package/templates/shop/lib/site.config.ts +198 -0
- package/templates/shop/lib/utils.ts +10 -0
- package/templates/shop/package.json +35 -0
- package/templates/shop/tsconfig.json +18 -0
- package/templates/waitlist/.env.example +12 -0
- package/templates/waitlist/AGENTS.md +61 -0
- package/templates/waitlist/README.md +81 -0
- package/templates/waitlist/app/auth-form.tsx +129 -0
- package/templates/waitlist/app/dashboard/dashboard-client.tsx +297 -0
- package/templates/waitlist/app/dashboard/page.tsx +70 -0
- package/templates/waitlist/app/error.tsx +26 -0
- package/templates/waitlist/app/globals.css +148 -0
- package/templates/waitlist/app/layout.tsx +158 -0
- package/templates/waitlist/app/login/page.tsx +39 -0
- package/templates/waitlist/app/not-found.tsx +19 -0
- package/templates/waitlist/app/page.tsx +119 -0
- package/templates/waitlist/app/robots.ts +12 -0
- package/templates/waitlist/app/sitemap.ts +9 -0
- package/templates/waitlist/app/waitlist-hero.tsx +219 -0
- package/templates/waitlist/app.ts +134 -0
- package/templates/waitlist/components/marketing.tsx +96 -0
- package/templates/waitlist/components/ui/button.tsx +56 -0
- package/templates/waitlist/components/ui/card.tsx +90 -0
- package/templates/waitlist/components.json +20 -0
- package/templates/waitlist/functions/joinWaitlist.ts +82 -0
- package/templates/waitlist/functions/waitlistStats.ts +75 -0
- package/templates/waitlist/gitignore +10 -0
- package/templates/waitlist/lib/owner.ts +26 -0
- package/templates/waitlist/lib/site.config.ts +178 -0
- package/templates/waitlist/lib/stats.ts +30 -0
- package/templates/waitlist/lib/utils.ts +10 -0
- package/templates/waitlist/package.json +34 -0
- package/templates/waitlist/tsconfig.json +18 -0
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import React, { useEffect, useMemo, useState } from "react";
|
|
4
|
+
import { db, callFn } from "@pylonsync/react";
|
|
5
|
+
import { useAuth } from "@pylonsync/client";
|
|
6
|
+
import type { ReservationRow, OwnerReservationsResult } from "@/lib/reservation";
|
|
7
|
+
|
|
8
|
+
// The owner's live reservations dashboard. Liveness rides the SAME public
|
|
9
|
+
// ReservationSlot markers the picker uses: `db.useQuery` re-renders the instant
|
|
10
|
+
// a table is taken or freed, triggering a re-fetch of the owner-gated
|
|
11
|
+
// `reservationsForOwner` — so new reservations appear and cancellations drop off
|
|
12
|
+
// without a refresh. Guest PII only travels through that gated call.
|
|
13
|
+
export function ReservationsDashboard({ userEmail }: { userEmail: string }) {
|
|
14
|
+
const { data: markers } = db.useQuery<{ id: string }>("ReservationSlot");
|
|
15
|
+
const liveKey = markers.length;
|
|
16
|
+
|
|
17
|
+
const [reservations, setReservations] = useState<ReservationRow[] | null>(null);
|
|
18
|
+
const [denied, setDenied] = useState(false);
|
|
19
|
+
const [error, setError] = useState<string | null>(null);
|
|
20
|
+
const [busyId, setBusyId] = useState<string | null>(null);
|
|
21
|
+
|
|
22
|
+
async function load() {
|
|
23
|
+
try {
|
|
24
|
+
const r = await callFn<OwnerReservationsResult>("reservationsForOwner", {});
|
|
25
|
+
if (!r.authorized) setDenied(true);
|
|
26
|
+
else {
|
|
27
|
+
setReservations(r.reservations);
|
|
28
|
+
setDenied(false);
|
|
29
|
+
setError(null);
|
|
30
|
+
}
|
|
31
|
+
} catch (e) {
|
|
32
|
+
setError(e instanceof Error ? e.message : String(e));
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
useEffect(() => {
|
|
37
|
+
void load();
|
|
38
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
39
|
+
}, [liveKey]);
|
|
40
|
+
|
|
41
|
+
async function act(id: string, fn: "confirmReservation" | "cancelReservation") {
|
|
42
|
+
setBusyId(id);
|
|
43
|
+
try {
|
|
44
|
+
await callFn(fn, { reservationId: id });
|
|
45
|
+
await load();
|
|
46
|
+
} finally {
|
|
47
|
+
setBusyId(null);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (denied) return <OwnerOnly email={userEmail} />;
|
|
52
|
+
if (error) {
|
|
53
|
+
return <div className="rounded-xl border border-red-200 bg-red-50 px-5 py-4 text-sm text-red-700">{error}</div>;
|
|
54
|
+
}
|
|
55
|
+
if (!reservations) return <Skeleton />;
|
|
56
|
+
|
|
57
|
+
const nowMs = Date.now();
|
|
58
|
+
const active = reservations.filter((r) => r.status !== "cancelled");
|
|
59
|
+
const upcoming = active.filter((r) => Date.parse(r.startsAt) >= nowMs - 3_600_000);
|
|
60
|
+
const todayKey = new Date().toLocaleDateString();
|
|
61
|
+
const todayCount = upcoming.filter((r) => new Date(r.startsAt).toLocaleDateString() === todayKey).length;
|
|
62
|
+
const covers = upcoming.reduce((sum, r) => sum + (r.partySize || 0), 0);
|
|
63
|
+
const pendingCount = upcoming.filter((r) => r.status === "pending").length;
|
|
64
|
+
|
|
65
|
+
return (
|
|
66
|
+
<div className="space-y-8">
|
|
67
|
+
<div>
|
|
68
|
+
<h1 className="text-xl font-semibold tracking-tight">Reservations</h1>
|
|
69
|
+
<p className="mt-1 text-sm text-zinc-500">
|
|
70
|
+
Live — new reservations appear the moment they happen; cancelling frees the table on the
|
|
71
|
+
site instantly.
|
|
72
|
+
</p>
|
|
73
|
+
</div>
|
|
74
|
+
|
|
75
|
+
<div className="grid gap-4 sm:grid-cols-4">
|
|
76
|
+
<Stat label="Upcoming" value={upcoming.length} />
|
|
77
|
+
<Stat label="Today" value={todayCount} />
|
|
78
|
+
<Stat label="Covers" value={covers} />
|
|
79
|
+
<Stat label="Awaiting confirm" value={pendingCount} />
|
|
80
|
+
</div>
|
|
81
|
+
|
|
82
|
+
<UpcomingList reservations={upcoming} busyId={busyId} onAct={act} />
|
|
83
|
+
</div>
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function Stat({ label, value }: { label: string; value: number }) {
|
|
88
|
+
return (
|
|
89
|
+
<div className="rounded-xl border border-zinc-200 bg-white p-4">
|
|
90
|
+
<div className="text-[11px] font-medium uppercase tracking-wide text-zinc-400">{label}</div>
|
|
91
|
+
<div className="mt-1 text-2xl font-semibold tabular-nums text-zinc-900">{value}</div>
|
|
92
|
+
</div>
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function UpcomingList({
|
|
97
|
+
reservations,
|
|
98
|
+
busyId,
|
|
99
|
+
onAct,
|
|
100
|
+
}: {
|
|
101
|
+
reservations: ReservationRow[];
|
|
102
|
+
busyId: string | null;
|
|
103
|
+
onAct: (id: string, fn: "confirmReservation" | "cancelReservation") => void;
|
|
104
|
+
}) {
|
|
105
|
+
const groups = useMemo(() => {
|
|
106
|
+
const m = new Map<string, ReservationRow[]>();
|
|
107
|
+
for (const r of reservations) {
|
|
108
|
+
const key = new Date(r.startsAt).toLocaleDateString(undefined, {
|
|
109
|
+
weekday: "long",
|
|
110
|
+
month: "short",
|
|
111
|
+
day: "numeric",
|
|
112
|
+
});
|
|
113
|
+
const arr = m.get(key) ?? [];
|
|
114
|
+
arr.push(r);
|
|
115
|
+
m.set(key, arr);
|
|
116
|
+
}
|
|
117
|
+
return Array.from(m.entries());
|
|
118
|
+
}, [reservations]);
|
|
119
|
+
|
|
120
|
+
if (reservations.length === 0) {
|
|
121
|
+
return (
|
|
122
|
+
<div className="rounded-xl border border-dashed border-zinc-300 p-10 text-center text-sm text-zinc-500">
|
|
123
|
+
No upcoming reservations yet. Share your site — new reservations land here live.
|
|
124
|
+
</div>
|
|
125
|
+
);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
return (
|
|
129
|
+
<div className="space-y-6">
|
|
130
|
+
{groups.map(([day, items]) => (
|
|
131
|
+
<div key={day}>
|
|
132
|
+
<h2 className="mb-2 text-[13px] font-semibold text-zinc-900">{day}</h2>
|
|
133
|
+
<div className="overflow-hidden rounded-xl border border-zinc-200">
|
|
134
|
+
{items.map((r, i) => (
|
|
135
|
+
<Item key={r.id} r={r} busy={busyId === r.id} onAct={onAct} last={i === items.length - 1} />
|
|
136
|
+
))}
|
|
137
|
+
</div>
|
|
138
|
+
</div>
|
|
139
|
+
))}
|
|
140
|
+
</div>
|
|
141
|
+
);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function Item({
|
|
145
|
+
r,
|
|
146
|
+
busy,
|
|
147
|
+
onAct,
|
|
148
|
+
last,
|
|
149
|
+
}: {
|
|
150
|
+
r: ReservationRow;
|
|
151
|
+
busy: boolean;
|
|
152
|
+
onAct: (id: string, fn: "confirmReservation" | "cancelReservation") => void;
|
|
153
|
+
last: boolean;
|
|
154
|
+
}) {
|
|
155
|
+
return (
|
|
156
|
+
<div className={"flex flex-wrap items-center gap-x-4 gap-y-2 px-4 py-3 " + (last ? "" : "border-b border-zinc-100")}>
|
|
157
|
+
<div className="w-20 shrink-0 text-[14px] font-semibold tabular-nums text-zinc-900">
|
|
158
|
+
{new Date(r.startsAt).toLocaleTimeString(undefined, { hour: "numeric", minute: "2-digit" })}
|
|
159
|
+
</div>
|
|
160
|
+
<div className="min-w-0 flex-1">
|
|
161
|
+
<div className="flex items-center gap-2">
|
|
162
|
+
<span className="text-[14px] font-medium text-zinc-900">{r.customerName}</span>
|
|
163
|
+
<span className="rounded-full bg-zinc-100 px-2 py-0.5 text-[11px] font-medium text-zinc-600">
|
|
164
|
+
party of {r.partySize}
|
|
165
|
+
</span>
|
|
166
|
+
<StatusBadge status={r.status} />
|
|
167
|
+
</div>
|
|
168
|
+
<div className="truncate text-[12.5px] text-zinc-500">
|
|
169
|
+
{r.customerEmail}
|
|
170
|
+
{r.customerPhone ? ` · ${r.customerPhone}` : ""}
|
|
171
|
+
{r.notes ? ` · “${r.notes}”` : ""}
|
|
172
|
+
</div>
|
|
173
|
+
</div>
|
|
174
|
+
<div className="flex items-center gap-2">
|
|
175
|
+
{r.status === "pending" ? (
|
|
176
|
+
<button
|
|
177
|
+
type="button"
|
|
178
|
+
disabled={busy}
|
|
179
|
+
onClick={() => onAct(r.id, "confirmReservation")}
|
|
180
|
+
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"
|
|
181
|
+
>
|
|
182
|
+
{busy ? "…" : "Confirm"}
|
|
183
|
+
</button>
|
|
184
|
+
) : null}
|
|
185
|
+
<button
|
|
186
|
+
type="button"
|
|
187
|
+
disabled={busy}
|
|
188
|
+
onClick={() => onAct(r.id, "cancelReservation")}
|
|
189
|
+
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"
|
|
190
|
+
>
|
|
191
|
+
Cancel
|
|
192
|
+
</button>
|
|
193
|
+
</div>
|
|
194
|
+
</div>
|
|
195
|
+
);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function StatusBadge({ status }: { status: string }) {
|
|
199
|
+
const tone =
|
|
200
|
+
status === "confirmed"
|
|
201
|
+
? "bg-green-50 text-green-700"
|
|
202
|
+
: status === "cancelled"
|
|
203
|
+
? "bg-zinc-100 text-zinc-500"
|
|
204
|
+
: "bg-amber-50 text-amber-700";
|
|
205
|
+
return <span className={"rounded-full px-2 py-0.5 text-[10px] font-medium capitalize " + tone}>{status}</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 owner can see reservations. Set{" "}
|
|
215
|
+
<code className="rounded bg-zinc-100 px-1.5 py-0.5 text-[12px]">PYLON_OWNER_EMAIL={email || "you@restaurant.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-36 animate-pulse rounded bg-zinc-100" />
|
|
255
|
+
<div className="grid gap-4 sm:grid-cols-4">
|
|
256
|
+
{[0, 1, 2, 3].map((i) => (
|
|
257
|
+
<div key={i} className="h-20 animate-pulse rounded-xl bg-zinc-100" />
|
|
258
|
+
))}
|
|
259
|
+
</div>
|
|
260
|
+
<div className="h-48 animate-pulse rounded-xl bg-zinc-100" />
|
|
261
|
+
</div>
|
|
262
|
+
);
|
|
263
|
+
}
|
|
@@ -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, ReservationsDashboard } 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 reservations + guest PII) lives in the
|
|
13
|
+
// `reservationsForOwner` function via `ctx.env`. Reading `auth` opts the render
|
|
14
|
+
// out of 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
|
+
<ReservationsDashboard 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">/ reservations</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
|
+
}
|
|
@@ -0,0 +1,151 @@
|
|
|
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 and a
|
|
7
|
+
// footer, both driven by lib/site.config.ts. `auth.user_id` is resolved
|
|
8
|
+
// server-side from the session cookie before any HTML is sent, so the owner
|
|
9
|
+
// sees "Dashboard" and everyone else sees "Sign in" — no flash, no client fetch.
|
|
10
|
+
interface LayoutProps {
|
|
11
|
+
children: React.ReactNode;
|
|
12
|
+
url: string;
|
|
13
|
+
auth: PageAuth;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export default function RootLayout({ children, url, auth }: LayoutProps) {
|
|
17
|
+
// A guest session (minted by <EnsureGuest> for the live picker) has a
|
|
18
|
+
// `guest_…` user id — that's an anonymous visitor, NOT the signed-in owner,
|
|
19
|
+
// so it shouldn't flip the nav to "Dashboard".
|
|
20
|
+
const signedIn = Boolean(auth?.user_id && !auth.user_id.startsWith("guest_"));
|
|
21
|
+
const { brand, colors } = siteConfig;
|
|
22
|
+
|
|
23
|
+
// Auth + dashboard render bare (no marketing chrome). Match the path PREFIX.
|
|
24
|
+
const path = (url ?? "").split("?")[0];
|
|
25
|
+
const BARE_PREFIXES = ["/login", "/dashboard"];
|
|
26
|
+
const isBare = BARE_PREFIXES.some((p) => path === p || path.startsWith(p + "/"));
|
|
27
|
+
|
|
28
|
+
return (
|
|
29
|
+
<html
|
|
30
|
+
lang="en"
|
|
31
|
+
style={
|
|
32
|
+
{
|
|
33
|
+
"--brand": colors.brand,
|
|
34
|
+
"--brand-soft": colors.brandSoft,
|
|
35
|
+
"--paper": colors.paper,
|
|
36
|
+
} as React.CSSProperties
|
|
37
|
+
}
|
|
38
|
+
>
|
|
39
|
+
<head>
|
|
40
|
+
<meta charSet="utf-8" />
|
|
41
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
42
|
+
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
|
43
|
+
<link rel="preconnect" href="https://fonts.gstatic.com" crossOrigin="anonymous" />
|
|
44
|
+
<link
|
|
45
|
+
rel="stylesheet"
|
|
46
|
+
href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap"
|
|
47
|
+
/>
|
|
48
|
+
</head>
|
|
49
|
+
<body className="flex min-h-screen flex-col bg-background text-foreground antialiased">
|
|
50
|
+
<SectionScroller />
|
|
51
|
+
{isBare ? (
|
|
52
|
+
children
|
|
53
|
+
) : (
|
|
54
|
+
<>
|
|
55
|
+
<header className="sticky top-0 z-30 border-b border-zinc-200/70 bg-white/85 backdrop-blur">
|
|
56
|
+
<div className="mx-auto flex h-14 max-w-5xl items-center justify-between px-6">
|
|
57
|
+
<Link href="/" className="flex items-center gap-2">
|
|
58
|
+
<span className="flex size-6 items-center justify-center rounded-[7px] bg-zinc-900 text-[13px] font-bold text-white">
|
|
59
|
+
{brand.letter}
|
|
60
|
+
</span>
|
|
61
|
+
<span className="text-[15px] font-semibold tracking-tight text-zinc-900">
|
|
62
|
+
{brand.name}
|
|
63
|
+
</span>
|
|
64
|
+
</Link>
|
|
65
|
+
<nav className="flex items-center gap-2">
|
|
66
|
+
<a
|
|
67
|
+
href="/#menu"
|
|
68
|
+
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"
|
|
69
|
+
>
|
|
70
|
+
Services
|
|
71
|
+
</a>
|
|
72
|
+
{signedIn ? (
|
|
73
|
+
<Link
|
|
74
|
+
href="/dashboard"
|
|
75
|
+
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"
|
|
76
|
+
>
|
|
77
|
+
Dashboard
|
|
78
|
+
</Link>
|
|
79
|
+
) : (
|
|
80
|
+
<>
|
|
81
|
+
<Link
|
|
82
|
+
href="/login"
|
|
83
|
+
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"
|
|
84
|
+
>
|
|
85
|
+
Sign in
|
|
86
|
+
</Link>
|
|
87
|
+
<a
|
|
88
|
+
href="/#reserve"
|
|
89
|
+
className="inline-flex items-center rounded-full bg-brand px-3.5 py-1.5 text-[13px] font-medium text-white transition-colors hover:opacity-90"
|
|
90
|
+
>
|
|
91
|
+
{siteConfig.hero.ctaLabel}
|
|
92
|
+
</a>
|
|
93
|
+
</>
|
|
94
|
+
)}
|
|
95
|
+
</nav>
|
|
96
|
+
</div>
|
|
97
|
+
</header>
|
|
98
|
+
|
|
99
|
+
<main className="flex-1">{children}</main>
|
|
100
|
+
|
|
101
|
+
<SiteFooter />
|
|
102
|
+
</>
|
|
103
|
+
)}
|
|
104
|
+
</body>
|
|
105
|
+
</html>
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function SiteFooter() {
|
|
110
|
+
const { brand, location } = siteConfig;
|
|
111
|
+
return (
|
|
112
|
+
<footer className="border-t border-zinc-200/70 bg-white">
|
|
113
|
+
<div className="mx-auto max-w-5xl px-6 py-12">
|
|
114
|
+
<div className="flex flex-col items-start justify-between gap-6 sm:flex-row">
|
|
115
|
+
<div className="max-w-sm">
|
|
116
|
+
<Link href="/" className="inline-flex items-center gap-2">
|
|
117
|
+
<span className="flex size-6 items-center justify-center rounded-[7px] bg-zinc-900 text-[13px] font-bold text-white">
|
|
118
|
+
{brand.letter}
|
|
119
|
+
</span>
|
|
120
|
+
<span className="text-[15px] font-semibold tracking-tight text-zinc-900">
|
|
121
|
+
{brand.name}
|
|
122
|
+
</span>
|
|
123
|
+
</Link>
|
|
124
|
+
<p className="mt-3 text-[13px] leading-relaxed text-zinc-500">
|
|
125
|
+
{brand.footerBlurb}
|
|
126
|
+
</p>
|
|
127
|
+
</div>
|
|
128
|
+
<div className="text-[13px] leading-relaxed text-zinc-500">
|
|
129
|
+
<div className="font-medium text-zinc-900">Visit</div>
|
|
130
|
+
<p className="mt-2 max-w-[14rem]">{location.address}</p>
|
|
131
|
+
<p className="mt-2">{location.phone}</p>
|
|
132
|
+
<a href={`mailto:${brand.email}`} className="mt-1 inline-block hover:text-zinc-900">
|
|
133
|
+
{brand.email}
|
|
134
|
+
</a>
|
|
135
|
+
</div>
|
|
136
|
+
</div>
|
|
137
|
+
<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">
|
|
138
|
+
<span>
|
|
139
|
+
© {new Date().getFullYear()} {brand.copyrightName}
|
|
140
|
+
</span>
|
|
141
|
+
<span>
|
|
142
|
+
Built with{" "}
|
|
143
|
+
<a href="https://pylonsync.com" className="font-medium text-zinc-600 hover:text-zinc-900">
|
|
144
|
+
Pylon
|
|
145
|
+
</a>
|
|
146
|
+
</span>
|
|
147
|
+
</div>
|
|
148
|
+
</div>
|
|
149
|
+
</footer>
|
|
150
|
+
);
|
|
151
|
+
}
|