@cimplify/cli 0.6.2 → 0.6.4
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/dist/{add-2DGJIRBT.mjs → add-NWGER62A.mjs} +1 -1
- package/dist/{chunk-SAESFNUJ.mjs → chunk-76TJ56KE.mjs} +1 -1
- package/dist/{chunk-XYEZPYCO.mjs → chunk-MGP2FUAK.mjs} +1 -1
- package/dist/chunk-TFEUFCEH.mjs +5707 -0
- package/dist/dispatcher.mjs +9 -9
- package/dist/{doctor-26CFG2CR.mjs → doctor-ZGW3VZOG.mjs} +2 -2
- package/dist/{explain-AIMUFEME.mjs → explain-4ITTI4FM.mjs} +1 -1
- package/dist/{introspect-5ELLE23M.mjs → introspect-DL2HE7DG.mjs} +2 -2
- package/dist/{list-74VZJRQE.mjs → list-WNNKVK7V.mjs} +1 -1
- package/dist/{update-5E646EVY.mjs → update-EPRLX6FL.mjs} +1 -1
- package/package.json +1 -1
- package/templates/storefront-auto/app/about/page.tsx +1 -1
- package/templates/storefront-auto/app/faq/page.tsx +1 -1
- package/templates/storefront-auto/app/layout.tsx +15 -31
- package/templates/storefront-auto/app/orders/[id]/page.tsx +1 -1
- package/templates/storefront-auto/app/privacy/page.tsx +1 -1
- package/templates/storefront-auto/app/terms/page.tsx +1 -1
- package/templates/storefront-auto/components/cart-pill.tsx +2 -2
- package/templates/storefront-auto/components/category-grid.tsx +1 -1
- package/templates/storefront-auto/components/collection-strip.tsx +1 -1
- package/templates/storefront-auto/components/header.tsx +13 -7
- package/templates/storefront-auto/components/hero.tsx +1 -1
- package/templates/storefront-auto/components/json-ld.tsx +34 -0
- package/templates/storefront-auto/components/mobile-nav.tsx +113 -0
- package/templates/storefront-auto/components/policy-page.tsx +1 -1
- package/templates/storefront-auto/components/section-heading.tsx +2 -2
- package/templates/storefront-auto/package.json +1 -1
- package/templates/storefront-bakery/app/about/page.tsx +1 -1
- package/templates/storefront-bakery/app/categories/[slug]/page.tsx +2 -2
- package/templates/storefront-bakery/app/collections/[slug]/page.tsx +2 -2
- package/templates/storefront-bakery/app/error.tsx +1 -1
- package/templates/storefront-bakery/app/faq/page.tsx +1 -1
- package/templates/storefront-bakery/app/layout.tsx +5 -26
- package/templates/storefront-bakery/app/not-found.tsx +1 -1
- package/templates/storefront-bakery/app/orders/[id]/page.tsx +1 -1
- package/templates/storefront-bakery/app/page.tsx +2 -2
- package/templates/storefront-bakery/app/privacy/page.tsx +1 -1
- package/templates/storefront-bakery/app/terms/page.tsx +1 -1
- package/templates/storefront-bakery/components/cart-pill.tsx +2 -2
- package/templates/storefront-bakery/components/category-grid.tsx +1 -1
- package/templates/storefront-bakery/components/collection-strip.tsx +1 -1
- package/templates/storefront-bakery/components/footer.tsx +1 -1
- package/templates/storefront-bakery/components/header.tsx +15 -9
- package/templates/storefront-bakery/components/hero.tsx +1 -1
- package/templates/storefront-bakery/components/json-ld.tsx +34 -0
- package/templates/storefront-bakery/components/mobile-nav.tsx +113 -0
- package/templates/storefront-bakery/components/policy-page.tsx +1 -1
- package/templates/storefront-bakery/package.json +1 -1
- package/templates/storefront-fashion/app/about/page.tsx +1 -1
- package/templates/storefront-fashion/app/faq/page.tsx +1 -1
- package/templates/storefront-fashion/app/layout.tsx +15 -31
- package/templates/storefront-fashion/app/orders/[id]/page.tsx +1 -1
- package/templates/storefront-fashion/app/privacy/page.tsx +1 -1
- package/templates/storefront-fashion/app/terms/page.tsx +1 -1
- package/templates/storefront-fashion/components/cart-pill.tsx +2 -2
- package/templates/storefront-fashion/components/category-grid.tsx +1 -1
- package/templates/storefront-fashion/components/collection-strip.tsx +1 -1
- package/templates/storefront-fashion/components/feature-hero.tsx +1 -1
- package/templates/storefront-fashion/components/header.tsx +13 -7
- package/templates/storefront-fashion/components/hero.tsx +1 -1
- package/templates/storefront-fashion/components/json-ld.tsx +34 -0
- package/templates/storefront-fashion/components/mobile-nav.tsx +113 -0
- package/templates/storefront-fashion/components/policy-page.tsx +1 -1
- package/templates/storefront-fashion/components/section-heading.tsx +2 -2
- package/templates/storefront-fashion/package.json +1 -1
- package/templates/storefront-grocery/app/about/page.tsx +1 -1
- package/templates/storefront-grocery/app/categories/[slug]/page.tsx +2 -2
- package/templates/storefront-grocery/app/collections/[slug]/page.tsx +2 -2
- package/templates/storefront-grocery/app/error.tsx +1 -1
- package/templates/storefront-grocery/app/faq/page.tsx +1 -1
- package/templates/storefront-grocery/app/layout.tsx +10 -32
- package/templates/storefront-grocery/app/not-found.tsx +1 -1
- package/templates/storefront-grocery/app/orders/[id]/page.tsx +1 -1
- package/templates/storefront-grocery/app/page.tsx +2 -2
- package/templates/storefront-grocery/app/privacy/page.tsx +1 -1
- package/templates/storefront-grocery/app/terms/page.tsx +1 -1
- package/templates/storefront-grocery/components/cart-pill.tsx +2 -2
- package/templates/storefront-grocery/components/category-grid.tsx +1 -1
- package/templates/storefront-grocery/components/collection-strip.tsx +1 -1
- package/templates/storefront-grocery/components/footer.tsx +1 -1
- package/templates/storefront-grocery/components/header.tsx +15 -9
- package/templates/storefront-grocery/components/hero.tsx +1 -1
- package/templates/storefront-grocery/components/json-ld.tsx +34 -0
- package/templates/storefront-grocery/components/mobile-nav.tsx +113 -0
- package/templates/storefront-grocery/components/policy-page.tsx +1 -1
- package/templates/storefront-grocery/package.json +1 -1
- package/templates/storefront-pharmacy/app/about/page.tsx +1 -1
- package/templates/storefront-pharmacy/app/faq/page.tsx +1 -1
- package/templates/storefront-pharmacy/app/layout.tsx +15 -31
- package/templates/storefront-pharmacy/app/orders/[id]/page.tsx +1 -1
- package/templates/storefront-pharmacy/app/privacy/page.tsx +1 -1
- package/templates/storefront-pharmacy/app/terms/page.tsx +1 -1
- package/templates/storefront-pharmacy/components/cart-pill.tsx +2 -2
- package/templates/storefront-pharmacy/components/category-grid.tsx +1 -1
- package/templates/storefront-pharmacy/components/collection-strip.tsx +1 -1
- package/templates/storefront-pharmacy/components/header.tsx +13 -7
- package/templates/storefront-pharmacy/components/hero.tsx +1 -1
- package/templates/storefront-pharmacy/components/json-ld.tsx +34 -0
- package/templates/storefront-pharmacy/components/mobile-nav.tsx +113 -0
- package/templates/storefront-pharmacy/components/policy-page.tsx +1 -1
- package/templates/storefront-pharmacy/components/section-heading.tsx +2 -2
- package/templates/storefront-pharmacy/package.json +1 -1
- package/templates/storefront-restaurant/app/about/page.tsx +1 -1
- package/templates/storefront-restaurant/app/categories/[slug]/page.tsx +2 -2
- package/templates/storefront-restaurant/app/collections/[slug]/page.tsx +2 -2
- package/templates/storefront-restaurant/app/error.tsx +1 -1
- package/templates/storefront-restaurant/app/faq/page.tsx +1 -1
- package/templates/storefront-restaurant/app/layout.tsx +10 -31
- package/templates/storefront-restaurant/app/not-found.tsx +1 -1
- package/templates/storefront-restaurant/app/orders/[id]/page.tsx +1 -1
- package/templates/storefront-restaurant/app/page.tsx +2 -2
- package/templates/storefront-restaurant/app/privacy/page.tsx +1 -1
- package/templates/storefront-restaurant/app/terms/page.tsx +1 -1
- package/templates/storefront-restaurant/components/cart-pill.tsx +2 -2
- package/templates/storefront-restaurant/components/category-grid.tsx +1 -1
- package/templates/storefront-restaurant/components/collection-strip.tsx +1 -1
- package/templates/storefront-restaurant/components/footer.tsx +1 -1
- package/templates/storefront-restaurant/components/header.tsx +15 -9
- package/templates/storefront-restaurant/components/hero.tsx +1 -1
- package/templates/storefront-restaurant/components/json-ld.tsx +34 -0
- package/templates/storefront-restaurant/components/mobile-nav.tsx +113 -0
- package/templates/storefront-restaurant/components/policy-page.tsx +1 -1
- package/templates/storefront-restaurant/package.json +1 -1
- package/templates/storefront-retail/app/about/page.tsx +1 -1
- package/templates/storefront-retail/app/faq/page.tsx +1 -1
- package/templates/storefront-retail/app/layout.tsx +15 -31
- package/templates/storefront-retail/app/orders/[id]/page.tsx +1 -1
- package/templates/storefront-retail/app/privacy/page.tsx +1 -1
- package/templates/storefront-retail/app/terms/page.tsx +1 -1
- package/templates/storefront-retail/components/cart-pill.tsx +2 -2
- package/templates/storefront-retail/components/category-grid.tsx +1 -1
- package/templates/storefront-retail/components/collection-strip.tsx +1 -1
- package/templates/storefront-retail/components/header.tsx +13 -7
- package/templates/storefront-retail/components/hero.tsx +1 -1
- package/templates/storefront-retail/components/json-ld.tsx +34 -0
- package/templates/storefront-retail/components/mobile-nav.tsx +113 -0
- package/templates/storefront-retail/components/policy-page.tsx +1 -1
- package/templates/storefront-retail/components/section-heading.tsx +2 -2
- package/templates/storefront-retail/package.json +1 -1
- package/templates/storefront-services/app/about/page.tsx +1 -1
- package/templates/storefront-services/app/book/book-client.tsx +41 -105
- package/templates/storefront-services/app/categories/[slug]/page.tsx +2 -2
- package/templates/storefront-services/app/collections/[slug]/page.tsx +2 -2
- package/templates/storefront-services/app/error.tsx +1 -1
- package/templates/storefront-services/app/faq/page.tsx +1 -1
- package/templates/storefront-services/app/layout.tsx +10 -31
- package/templates/storefront-services/app/not-found.tsx +1 -1
- package/templates/storefront-services/app/orders/[id]/page.tsx +1 -1
- package/templates/storefront-services/app/page.tsx +2 -2
- package/templates/storefront-services/app/privacy/page.tsx +1 -1
- package/templates/storefront-services/app/terms/page.tsx +1 -1
- package/templates/storefront-services/components/cart-pill.tsx +2 -2
- package/templates/storefront-services/components/category-grid.tsx +1 -1
- package/templates/storefront-services/components/collection-strip.tsx +1 -1
- package/templates/storefront-services/components/footer.tsx +1 -1
- package/templates/storefront-services/components/header.tsx +15 -9
- package/templates/storefront-services/components/hero.tsx +1 -1
- package/templates/storefront-services/components/json-ld.tsx +34 -0
- package/templates/storefront-services/components/mobile-nav.tsx +113 -0
- package/templates/storefront-services/components/policy-page.tsx +1 -1
- package/templates/storefront-services/package.json +1 -1
- package/dist/chunk-PPU3YTG7.mjs +0 -5707
|
@@ -1,26 +1,19 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
|
-
import {
|
|
3
|
+
import { useState } from "react";
|
|
4
4
|
import { useRouter } from "next/navigation";
|
|
5
5
|
import type { Product } from "@cimplify/sdk";
|
|
6
|
-
import {
|
|
7
|
-
|
|
8
|
-
interface Slot {
|
|
9
|
-
date: Date;
|
|
10
|
-
label: string;
|
|
11
|
-
}
|
|
6
|
+
import type { AvailableSlot } from "@cimplify/sdk";
|
|
7
|
+
import { useCart, DateSlotPicker } from "@cimplify/sdk/react";
|
|
12
8
|
|
|
13
9
|
/**
|
|
14
|
-
*
|
|
15
|
-
* 1. Pick a treatment (left rail)
|
|
16
|
-
* 2.
|
|
17
|
-
*
|
|
18
|
-
*
|
|
10
|
+
* Booking flow:
|
|
11
|
+
* 1. Pick a treatment (left rail) — drives the SDK availability fetch.
|
|
12
|
+
* 2. SDK <DateSlotPicker> handles date + slot selection, fetching real
|
|
13
|
+
* availability via `useServiceAvailability` against the configured
|
|
14
|
+
* backend (mock in dev, Cimplify scheduling API in prod).
|
|
15
|
+
* 3. Add to cart with the chosen slot as a cart-item note;
|
|
19
16
|
* Cimplify Checkout finalises the booking.
|
|
20
|
-
*
|
|
21
|
-
* The slot grid is generated client-side as a placeholder. In production,
|
|
22
|
-
* call `useAvailableSlots({ serviceId, date })` from `@cimplify/sdk/react`
|
|
23
|
-
* to fetch real availability from the Cimplify scheduling API.
|
|
24
17
|
*/
|
|
25
18
|
export function BookClient({ treatments }: { treatments: Product[] }) {
|
|
26
19
|
const router = useRouter();
|
|
@@ -28,41 +21,22 @@ export function BookClient({ treatments }: { treatments: Product[] }) {
|
|
|
28
21
|
const [selectedTreatment, setSelectedTreatment] = useState<Product | undefined>(
|
|
29
22
|
treatments[0],
|
|
30
23
|
);
|
|
31
|
-
const [
|
|
32
|
-
const [selectedSlotKey, setSelectedSlotKey] = useState<string | null>(null);
|
|
24
|
+
const [selectedSlot, setSelectedSlot] = useState<AvailableSlot | null>(null);
|
|
33
25
|
const [submitting, setSubmitting] = useState(false);
|
|
34
26
|
|
|
35
|
-
const dates = useMemo(() => {
|
|
36
|
-
const now = new Date();
|
|
37
|
-
return Array.from({ length: 14 }).map((_, i) => {
|
|
38
|
-
const d = new Date(now);
|
|
39
|
-
d.setDate(now.getDate() + i);
|
|
40
|
-
d.setHours(0, 0, 0, 0);
|
|
41
|
-
return d;
|
|
42
|
-
});
|
|
43
|
-
}, []);
|
|
44
|
-
|
|
45
|
-
const slots = useMemo<Slot[]>(() => {
|
|
46
|
-
const out: Slot[] = [];
|
|
47
|
-
for (let h = 10; h <= 19; h++) {
|
|
48
|
-
for (const m of [0, 30]) {
|
|
49
|
-
const d = new Date(selectedDate);
|
|
50
|
-
d.setHours(h, m, 0, 0);
|
|
51
|
-
const label = `${String(h).padStart(2, "0")}:${String(m).padStart(2, "0")}`;
|
|
52
|
-
out.push({ date: d, label });
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
return out;
|
|
56
|
-
}, [selectedDate]);
|
|
57
|
-
|
|
58
|
-
const slotKey = (s: Slot) => s.date.toISOString();
|
|
59
|
-
|
|
60
27
|
async function confirm() {
|
|
61
|
-
if (!selectedTreatment || !
|
|
28
|
+
if (!selectedTreatment || !selectedSlot) return;
|
|
62
29
|
setSubmitting(true);
|
|
63
30
|
try {
|
|
31
|
+
const slotLabel = new Date(selectedSlot.start_time).toLocaleString(undefined, {
|
|
32
|
+
weekday: "short",
|
|
33
|
+
month: "short",
|
|
34
|
+
day: "numeric",
|
|
35
|
+
hour: "2-digit",
|
|
36
|
+
minute: "2-digit",
|
|
37
|
+
});
|
|
64
38
|
await addItem(selectedTreatment, 1, {
|
|
65
|
-
|
|
39
|
+
specialInstructions: `Booked for ${slotLabel}`,
|
|
66
40
|
});
|
|
67
41
|
router.push("/checkout");
|
|
68
42
|
} catch {
|
|
@@ -92,7 +66,10 @@ export function BookClient({ treatments }: { treatments: Product[] }) {
|
|
|
92
66
|
<button
|
|
93
67
|
key={t.id}
|
|
94
68
|
type="button"
|
|
95
|
-
onClick={() =>
|
|
69
|
+
onClick={() => {
|
|
70
|
+
setSelectedTreatment(t);
|
|
71
|
+
setSelectedSlot(null);
|
|
72
|
+
}}
|
|
96
73
|
className={[
|
|
97
74
|
"w-full text-left rounded-2xl border p-4 transition-colors",
|
|
98
75
|
active
|
|
@@ -120,73 +97,32 @@ export function BookClient({ treatments }: { treatments: Product[] }) {
|
|
|
120
97
|
</div>
|
|
121
98
|
</div>
|
|
122
99
|
|
|
123
|
-
{/* Date + slots */}
|
|
124
|
-
<div className="rounded-2xl border border-border bg-card p-6">
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
}}
|
|
139
|
-
className={[
|
|
140
|
-
"flex flex-col items-center justify-center py-2 rounded-md transition-colors",
|
|
141
|
-
active
|
|
142
|
-
? "bg-foreground text-background"
|
|
143
|
-
: "bg-background hover:bg-muted text-foreground",
|
|
144
|
-
].join(" ")}
|
|
145
|
-
>
|
|
146
|
-
<span className="text-[10px] uppercase tracking-wider opacity-60">
|
|
147
|
-
{d.toLocaleString(undefined, { weekday: "short" })}
|
|
148
|
-
</span>
|
|
149
|
-
<span className="text-base font-semibold tabular-nums">{d.getDate()}</span>
|
|
150
|
-
</button>
|
|
151
|
-
);
|
|
152
|
-
})}
|
|
153
|
-
</div>
|
|
154
|
-
|
|
155
|
-
<p className="text-[11px] font-mono uppercase tracking-[0.16em] text-muted-foreground mb-3">
|
|
156
|
-
Slot
|
|
157
|
-
</p>
|
|
158
|
-
<div className="grid grid-cols-3 sm:grid-cols-4 gap-2">
|
|
159
|
-
{slots.map((s) => {
|
|
160
|
-
const key = slotKey(s);
|
|
161
|
-
const active = selectedSlotKey === key;
|
|
162
|
-
return (
|
|
163
|
-
<button
|
|
164
|
-
key={key}
|
|
165
|
-
type="button"
|
|
166
|
-
onClick={() => setSelectedSlotKey(key)}
|
|
167
|
-
className={[
|
|
168
|
-
"py-2 rounded-md text-sm tabular-nums transition-colors",
|
|
169
|
-
active
|
|
170
|
-
? "bg-primary text-primary-foreground"
|
|
171
|
-
: "bg-background border border-border hover:border-primary",
|
|
172
|
-
].join(" ")}
|
|
173
|
-
>
|
|
174
|
-
{s.label}
|
|
175
|
-
</button>
|
|
176
|
-
);
|
|
177
|
-
})}
|
|
178
|
-
</div>
|
|
100
|
+
{/* Date + slots — SDK <DateSlotPicker> fetches real availability */}
|
|
101
|
+
<div className="rounded-2xl border border-border bg-card p-4 sm:p-6">
|
|
102
|
+
{selectedTreatment ? (
|
|
103
|
+
<DateSlotPicker
|
|
104
|
+
serviceId={selectedTreatment.id}
|
|
105
|
+
selectedSlot={selectedSlot}
|
|
106
|
+
onSlotSelect={(slot) => setSelectedSlot(slot)}
|
|
107
|
+
daysToShow={selectedTreatment.scheduling_mode === "multi_day" ? 14 : 7}
|
|
108
|
+
schedulingMode={selectedTreatment.scheduling_mode}
|
|
109
|
+
durationUnit={selectedTreatment.duration_unit}
|
|
110
|
+
durationValue={selectedTreatment.duration_value}
|
|
111
|
+
/>
|
|
112
|
+
) : (
|
|
113
|
+
<p className="text-sm text-muted-foreground">Pick a treatment to see availability.</p>
|
|
114
|
+
)}
|
|
179
115
|
|
|
180
116
|
<button
|
|
181
117
|
type="button"
|
|
182
118
|
onClick={confirm}
|
|
183
|
-
disabled={!selectedTreatment || !
|
|
119
|
+
disabled={!selectedTreatment || !selectedSlot || submitting}
|
|
184
120
|
className="w-full mt-6 inline-flex items-center justify-center gap-2 px-5 py-3 rounded-md bg-primary text-primary-foreground font-semibold text-sm hover:bg-primary/90 transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
|
|
185
121
|
>
|
|
186
122
|
{submitting
|
|
187
123
|
? "Confirming…"
|
|
188
|
-
:
|
|
189
|
-
? `Book ${selectedTreatment?.name} at ${new Date(
|
|
124
|
+
: selectedSlot
|
|
125
|
+
? `Book ${selectedTreatment?.name} at ${new Date(selectedSlot.start_time).toLocaleTimeString(undefined, { hour: "2-digit", minute: "2-digit" })}`
|
|
190
126
|
: "Pick a slot to book"}
|
|
191
127
|
</button>
|
|
192
128
|
</div>
|
|
@@ -71,7 +71,7 @@ async function CategoryContent({
|
|
|
71
71
|
|
|
72
72
|
const { category, products } = data;
|
|
73
73
|
return (
|
|
74
|
-
<section className="max-w-7xl mx-auto px-8 pt-12">
|
|
74
|
+
<section className="max-w-7xl mx-auto px-6 sm:px-8 pt-12">
|
|
75
75
|
<header className="mb-8 text-center">
|
|
76
76
|
<p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-primary mb-2">
|
|
77
77
|
Category
|
|
@@ -102,7 +102,7 @@ async function CategoryContent({
|
|
|
102
102
|
|
|
103
103
|
function CategorySkeleton() {
|
|
104
104
|
return (
|
|
105
|
-
<section className="max-w-7xl mx-auto px-8 pt-12">
|
|
105
|
+
<section className="max-w-7xl mx-auto px-6 sm:px-8 pt-12">
|
|
106
106
|
<header className="mb-8 text-center">
|
|
107
107
|
<div className="mx-auto h-3 w-20 bg-muted rounded mb-2 animate-pulse" />
|
|
108
108
|
<div className="mx-auto h-10 w-64 bg-muted rounded mb-2 animate-pulse" />
|
|
@@ -71,7 +71,7 @@ async function CollectionContent({
|
|
|
71
71
|
|
|
72
72
|
const { collection, products } = data;
|
|
73
73
|
return (
|
|
74
|
-
<section className="max-w-7xl mx-auto px-8 pt-12">
|
|
74
|
+
<section className="max-w-7xl mx-auto px-6 sm:px-8 pt-12">
|
|
75
75
|
<header className="mb-8 text-center">
|
|
76
76
|
<p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-primary mb-2">
|
|
77
77
|
Collection
|
|
@@ -102,7 +102,7 @@ async function CollectionContent({
|
|
|
102
102
|
|
|
103
103
|
function CollectionSkeleton() {
|
|
104
104
|
return (
|
|
105
|
-
<section className="max-w-7xl mx-auto px-8 pt-12">
|
|
105
|
+
<section className="max-w-7xl mx-auto px-6 sm:px-8 pt-12">
|
|
106
106
|
<header className="mb-8 text-center">
|
|
107
107
|
<div className="mx-auto h-3 w-20 bg-muted rounded mb-2 animate-pulse" />
|
|
108
108
|
<div className="mx-auto h-10 w-64 bg-muted rounded mb-2 animate-pulse" />
|
|
@@ -24,7 +24,7 @@ export default function GlobalError({
|
|
|
24
24
|
}, [error]);
|
|
25
25
|
|
|
26
26
|
return (
|
|
27
|
-
<section className="max-w-2xl mx-auto px-8 py-20 text-center">
|
|
27
|
+
<section className="max-w-2xl mx-auto px-6 sm:px-8 py-20 text-center">
|
|
28
28
|
<p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-primary mb-3">
|
|
29
29
|
Something broke
|
|
30
30
|
</p>
|
|
@@ -9,7 +9,7 @@ export const metadata: Metadata = {
|
|
|
9
9
|
export default function FaqPage() {
|
|
10
10
|
const f = brand.faq;
|
|
11
11
|
return (
|
|
12
|
-
<article className="max-w-3xl mx-auto px-8 py-16">
|
|
12
|
+
<article className="max-w-3xl mx-auto px-6 sm:px-8 py-16">
|
|
13
13
|
<p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-primary mb-2">
|
|
14
14
|
{f.eyebrow}
|
|
15
15
|
</p>
|
|
@@ -6,6 +6,7 @@ import { Header } from "@/components/header";
|
|
|
6
6
|
import { Footer } from "@/components/footer";
|
|
7
7
|
import { ProductModal } from "@/components/product-modal";
|
|
8
8
|
import { CartDrawer } from "@/components/cart-drawer";
|
|
9
|
+
import { OrganizationJsonLd } from "@/components/json-ld";
|
|
9
10
|
import { Suspense } from "react";
|
|
10
11
|
import { brand } from "@/lib/brand";
|
|
11
12
|
import { getSiteUrl } from "@/lib/site-url";
|
|
@@ -27,51 +28,29 @@ export async function generateMetadata(): Promise<Metadata> {
|
|
|
27
28
|
return {
|
|
28
29
|
metadataBase: new URL(siteUrl),
|
|
29
30
|
title: {
|
|
30
|
-
|
|
31
|
-
|
|
31
|
+
default: brand.name,
|
|
32
|
+
template: `%s — ${brand.name}`,
|
|
32
33
|
},
|
|
33
34
|
description: brand.description,
|
|
34
35
|
openGraph: {
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
36
|
+
type: "website",
|
|
37
|
+
siteName: brand.name,
|
|
38
|
+
locale: brand.locale,
|
|
38
39
|
},
|
|
39
40
|
twitter: { card: "summary_large_image" },
|
|
40
41
|
};
|
|
41
42
|
}
|
|
42
43
|
|
|
43
|
-
|
|
44
|
-
const siteUrl = await getSiteUrl();
|
|
45
|
-
return {
|
|
46
|
-
"@context": "https://schema.org",
|
|
47
|
-
"@type": brand.schemaType,
|
|
48
|
-
name: brand.name,
|
|
49
|
-
url: siteUrl,
|
|
50
|
-
description: brand.description,
|
|
51
|
-
email: brand.contact.email,
|
|
52
|
-
telephone: brand.contact.phoneTel,
|
|
53
|
-
address: {
|
|
54
|
-
"@type": "PostalAddress",
|
|
55
|
-
streetAddress: brand.contact.streetAddress,
|
|
56
|
-
addressLocality: brand.contact.city,
|
|
57
|
-
addressCountry: brand.contact.countryCode,
|
|
58
|
-
},
|
|
59
|
-
sameAs: brand.socials.map((s) => s.href),
|
|
60
|
-
};
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
export default async function RootLayout({ children }: { children: React.ReactNode }) {
|
|
64
|
-
const ld = await organizationLd();
|
|
44
|
+
export default function RootLayout({ children }: { children: React.ReactNode }) {
|
|
65
45
|
return (
|
|
66
46
|
<html lang="en" suppressHydrationWarning className={`${inter.variable} ${fraunces.variable}`}>
|
|
67
47
|
<body
|
|
68
48
|
suppressHydrationWarning
|
|
69
49
|
className="min-h-screen flex flex-col bg-background text-foreground font-sans"
|
|
70
50
|
>
|
|
71
|
-
<
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
/>
|
|
51
|
+
<Suspense fallback={null}>
|
|
52
|
+
<OrganizationJsonLd />
|
|
53
|
+
</Suspense>
|
|
75
54
|
<Providers>
|
|
76
55
|
<Header />
|
|
77
56
|
<main className="flex-1 pb-12 w-full">{children}</main>
|
|
@@ -9,7 +9,7 @@ export const metadata: Metadata = {
|
|
|
9
9
|
|
|
10
10
|
export default function NotFound() {
|
|
11
11
|
return (
|
|
12
|
-
<section className="max-w-2xl mx-auto px-8 py-20 text-center">
|
|
12
|
+
<section className="max-w-2xl mx-auto px-6 sm:px-8 py-20 text-center">
|
|
13
13
|
<p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-primary mb-3">
|
|
14
14
|
404
|
|
15
15
|
</p>
|
|
@@ -3,7 +3,7 @@ import Link from "next/link";
|
|
|
3
3
|
export default async function OrderPage({ params }: { params: Promise<{ id: string }> }) {
|
|
4
4
|
const { id } = await params;
|
|
5
5
|
return (
|
|
6
|
-
<section className="max-w-2xl mx-auto px-8 py-20 text-center">
|
|
6
|
+
<section className="max-w-2xl mx-auto px-6 sm:px-8 py-20 text-center">
|
|
7
7
|
<h1 className="font-serif text-3xl mt-0 mb-3">Thanks — your order is confirmed</h1>
|
|
8
8
|
<p className="text-muted-foreground">
|
|
9
9
|
Order <code className="font-mono text-foreground">{id}</code>
|
|
@@ -72,7 +72,7 @@ export default async function HomePage() {
|
|
|
72
72
|
|
|
73
73
|
function StripSkeleton({ title }: { title: string }) {
|
|
74
74
|
return (
|
|
75
|
-
<section className="max-w-7xl mx-auto px-8 pt-12">
|
|
75
|
+
<section className="max-w-7xl mx-auto px-6 sm:px-8 pt-12">
|
|
76
76
|
<h2 className="font-serif text-[28px] font-semibold m-0 mb-5">{title}</h2>
|
|
77
77
|
<div className="grid grid-flow-col auto-cols-[minmax(220px,1fr)] gap-4 overflow-x-auto pb-2">
|
|
78
78
|
{Array.from({ length: 5 }).map((_, i) => (
|
|
@@ -85,7 +85,7 @@ function StripSkeleton({ title }: { title: string }) {
|
|
|
85
85
|
|
|
86
86
|
function CategoryGridSkeleton() {
|
|
87
87
|
return (
|
|
88
|
-
<section className="max-w-7xl mx-auto px-8 pt-14">
|
|
88
|
+
<section className="max-w-7xl mx-auto px-6 sm:px-8 pt-14">
|
|
89
89
|
<div className="h-8 w-48 bg-muted rounded mb-5 animate-pulse" />
|
|
90
90
|
<div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-4">
|
|
91
91
|
{Array.from({ length: 6 }).map((_, i) => (
|
|
@@ -9,7 +9,7 @@ export const metadata: Metadata = {
|
|
|
9
9
|
export default function PrivacyPage() {
|
|
10
10
|
const p = brand.privacy;
|
|
11
11
|
return (
|
|
12
|
-
<article className="max-w-3xl mx-auto px-8 py-16 prose prose-lg max-w-none">
|
|
12
|
+
<article className="max-w-3xl mx-auto px-6 sm:px-8 py-16 prose prose-lg max-w-none">
|
|
13
13
|
<p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-primary mb-2 not-prose">
|
|
14
14
|
{p.eyebrow}
|
|
15
15
|
</p>
|
|
@@ -9,7 +9,7 @@ export const metadata: Metadata = {
|
|
|
9
9
|
export default function TermsPage() {
|
|
10
10
|
const t = brand.terms;
|
|
11
11
|
return (
|
|
12
|
-
<article className="max-w-3xl mx-auto px-8 py-16 prose prose-lg max-w-none">
|
|
12
|
+
<article className="max-w-3xl mx-auto px-6 sm:px-8 py-16 prose prose-lg max-w-none">
|
|
13
13
|
<p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-primary mb-2 not-prose">
|
|
14
14
|
{t.eyebrow}
|
|
15
15
|
</p>
|
|
@@ -17,7 +17,7 @@ export function CartPill() {
|
|
|
17
17
|
type="button"
|
|
18
18
|
onClick={open}
|
|
19
19
|
aria-label={`Open cart, ${count} ${count === 1 ? "item" : "items"}`}
|
|
20
|
-
className="inline-flex items-center gap-1.5 px-
|
|
20
|
+
className="inline-flex items-center gap-1.5 px-4 py-2.5 sm:py-2 rounded-full bg-foreground text-background text-xs font-semibold tracking-wide transition-transform hover:scale-105 cursor-pointer"
|
|
21
21
|
>
|
|
22
22
|
Cart · {count}
|
|
23
23
|
</button>
|
|
@@ -28,7 +28,7 @@ export function CartPillSkeleton() {
|
|
|
28
28
|
return (
|
|
29
29
|
<span
|
|
30
30
|
aria-hidden
|
|
31
|
-
className="inline-flex items-center gap-1.5 px-
|
|
31
|
+
className="inline-flex items-center gap-1.5 px-4 py-2.5 sm:py-2 rounded-full bg-foreground/80 text-background text-xs font-semibold tracking-wide"
|
|
32
32
|
>
|
|
33
33
|
Cart · …
|
|
34
34
|
</span>
|
|
@@ -11,7 +11,7 @@ import type { Category } from "@cimplify/sdk";
|
|
|
11
11
|
export function CategoryGrid({ categories }: { categories?: Category[] }) {
|
|
12
12
|
const router = useRouter();
|
|
13
13
|
return (
|
|
14
|
-
<section className="max-w-7xl mx-auto px-8 pt-14">
|
|
14
|
+
<section className="max-w-7xl mx-auto px-6 sm:px-8 pt-14">
|
|
15
15
|
<h2 className="font-serif text-[28px] font-semibold m-0 mb-5">Browse the menu</h2>
|
|
16
16
|
<SdkCategoryGrid
|
|
17
17
|
categories={categories}
|
|
@@ -16,7 +16,7 @@ interface CollectionStripProps {
|
|
|
16
16
|
export function CollectionStrip({ collection, products, collectionHref }: CollectionStripProps) {
|
|
17
17
|
if (products.length === 0) return null;
|
|
18
18
|
return (
|
|
19
|
-
<section className="max-w-7xl mx-auto px-8 pt-12">
|
|
19
|
+
<section className="max-w-7xl mx-auto px-6 sm:px-8 pt-12">
|
|
20
20
|
<header className="grid grid-cols-[1fr_auto] items-end gap-4 mb-5">
|
|
21
21
|
<h2 className="font-serif text-[28px] font-semibold m-0">{collection.name}</h2>
|
|
22
22
|
{collectionHref && (
|
|
@@ -51,7 +51,7 @@ export async function Footer() {
|
|
|
51
51
|
"use cache";
|
|
52
52
|
const year = new Date().getFullYear();
|
|
53
53
|
return (
|
|
54
|
-
<footer className="mt-12 px-8 py-10 text-xs text-muted-foreground border-t border-border bg-card">
|
|
54
|
+
<footer className="mt-12 px-6 sm:px-8 py-10 text-xs text-muted-foreground border-t border-border bg-card">
|
|
55
55
|
<div className="max-w-7xl mx-auto">
|
|
56
56
|
<div className="grid gap-10 md:grid-cols-[1.4fr_repeat(4,1fr)]">
|
|
57
57
|
<div>
|
|
@@ -2,6 +2,7 @@ import Link from "next/link";
|
|
|
2
2
|
import { Suspense } from "react";
|
|
3
3
|
import { NavLink } from "./nav-link";
|
|
4
4
|
import { CartPill, CartPillSkeleton } from "./cart-pill";
|
|
5
|
+
import { MobileNav } from "./mobile-nav";
|
|
5
6
|
import { brand } from "@/lib/brand";
|
|
6
7
|
|
|
7
8
|
/**
|
|
@@ -11,25 +12,30 @@ import { brand } from "@/lib/brand";
|
|
|
11
12
|
*/
|
|
12
13
|
export function Header() {
|
|
13
14
|
return (
|
|
14
|
-
<header className="sticky top-0 z-30 flex items-center justify-between px-8 py-4 border-b border-border bg-background/90 backdrop-blur-md">
|
|
15
|
+
<header className="sticky top-0 z-30 flex items-center justify-between px-6 sm:px-8 py-4 border-b border-border bg-background/90 backdrop-blur-md">
|
|
15
16
|
<Link href="/" className="flex items-baseline gap-2">
|
|
16
17
|
<span className="font-serif text-[22px] font-semibold -tracking-[0.02em]">
|
|
17
18
|
{brand.shortName}
|
|
18
19
|
</span>
|
|
19
|
-
<span className="text-[11px] font-medium uppercase tracking-[0.12em] text-muted-foreground">
|
|
20
|
+
<span className="hidden sm:inline text-[11px] font-medium uppercase tracking-[0.12em] text-muted-foreground">
|
|
20
21
|
{brand.microTag}
|
|
21
22
|
</span>
|
|
22
23
|
</Link>
|
|
23
|
-
<
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
<
|
|
27
|
-
|
|
28
|
-
|
|
24
|
+
<div className="flex items-center gap-3 sm:gap-6">
|
|
25
|
+
<nav className="hidden sm:flex items-center gap-6">
|
|
26
|
+
{brand.header.nav.map((link) => (
|
|
27
|
+
<Suspense key={link.href} fallback={<NavLinkFallback>{link.label}</NavLinkFallback>}>
|
|
28
|
+
<NavLink href={link.href}>{link.label}</NavLink>
|
|
29
|
+
</Suspense>
|
|
30
|
+
))}
|
|
31
|
+
</nav>
|
|
29
32
|
<Suspense fallback={<CartPillSkeleton />}>
|
|
30
33
|
<CartPill />
|
|
31
34
|
</Suspense>
|
|
32
|
-
|
|
35
|
+
<div className="sm:hidden">
|
|
36
|
+
<MobileNav />
|
|
37
|
+
</div>
|
|
38
|
+
</div>
|
|
33
39
|
</header>
|
|
34
40
|
);
|
|
35
41
|
}
|
|
@@ -6,7 +6,7 @@ interface HeroProps {
|
|
|
6
6
|
|
|
7
7
|
export function Hero({ badge, title, subtitle }: HeroProps) {
|
|
8
8
|
return (
|
|
9
|
-
<section className="px-8 py-16 text-center bg-gradient-to-b from-accent to-background">
|
|
9
|
+
<section className="px-6 sm:px-8 py-16 text-center bg-gradient-to-b from-accent to-background">
|
|
10
10
|
{badge && (
|
|
11
11
|
<span className="inline-block mb-4 px-3.5 py-1.5 rounded-full bg-card border border-accent text-accent-foreground text-[11px] font-semibold uppercase tracking-[0.12em]">
|
|
12
12
|
{badge}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { brand } from "@/lib/brand";
|
|
2
|
+
import { getSiteUrl } from "@/lib/site-url";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Streams the organization JSON-LD script tag in async so it doesn't block
|
|
6
|
+
* the rest of the layout shell. `getSiteUrl` reads request headers, and
|
|
7
|
+
* any await on dynamic data inside `RootLayout` makes Next 16 mark the
|
|
8
|
+
* whole route as blocking.
|
|
9
|
+
*/
|
|
10
|
+
export async function OrganizationJsonLd(): Promise<React.ReactElement> {
|
|
11
|
+
const siteUrl = await getSiteUrl();
|
|
12
|
+
const ld = {
|
|
13
|
+
"@context": "https://schema.org",
|
|
14
|
+
"@type": brand.schemaType,
|
|
15
|
+
name: brand.name,
|
|
16
|
+
url: siteUrl,
|
|
17
|
+
description: brand.description,
|
|
18
|
+
email: brand.contact.email,
|
|
19
|
+
telephone: brand.contact.phoneTel,
|
|
20
|
+
address: {
|
|
21
|
+
"@type": "PostalAddress",
|
|
22
|
+
streetAddress: brand.contact.streetAddress,
|
|
23
|
+
addressLocality: brand.contact.city,
|
|
24
|
+
addressCountry: brand.contact.countryCode,
|
|
25
|
+
},
|
|
26
|
+
sameAs: brand.socials.map((s) => s.href),
|
|
27
|
+
};
|
|
28
|
+
return (
|
|
29
|
+
<script
|
|
30
|
+
type="application/ld+json"
|
|
31
|
+
dangerouslySetInnerHTML={{ __html: JSON.stringify(ld) }}
|
|
32
|
+
/>
|
|
33
|
+
);
|
|
34
|
+
}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useEffect, useState } from "react";
|
|
4
|
+
import Link from "next/link";
|
|
5
|
+
import { brand } from "@/lib/brand";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Hamburger button + slide-in drawer for narrow viewports. Header hides
|
|
9
|
+
* its inline nav links below `sm` and renders this in their place; the
|
|
10
|
+
* cart pill stays in the header chrome.
|
|
11
|
+
*/
|
|
12
|
+
export function MobileNav() {
|
|
13
|
+
const [open, setOpen] = useState(false);
|
|
14
|
+
|
|
15
|
+
useEffect(() => {
|
|
16
|
+
if (!open) return;
|
|
17
|
+
const onKey = (event: KeyboardEvent) => {
|
|
18
|
+
if (event.key === "Escape") setOpen(false);
|
|
19
|
+
};
|
|
20
|
+
document.addEventListener("keydown", onKey);
|
|
21
|
+
const previousOverflow = document.body.style.overflow;
|
|
22
|
+
document.body.style.overflow = "hidden";
|
|
23
|
+
return () => {
|
|
24
|
+
document.removeEventListener("keydown", onKey);
|
|
25
|
+
document.body.style.overflow = previousOverflow;
|
|
26
|
+
};
|
|
27
|
+
}, [open]);
|
|
28
|
+
|
|
29
|
+
return (
|
|
30
|
+
<>
|
|
31
|
+
<button
|
|
32
|
+
type="button"
|
|
33
|
+
onClick={() => setOpen(true)}
|
|
34
|
+
aria-label="Open menu"
|
|
35
|
+
aria-expanded={open}
|
|
36
|
+
aria-controls="mobile-nav-drawer"
|
|
37
|
+
className="grid place-items-center w-11 h-11 -mr-2 rounded-md text-foreground hover:bg-muted transition-colors"
|
|
38
|
+
>
|
|
39
|
+
<svg
|
|
40
|
+
width="20"
|
|
41
|
+
height="20"
|
|
42
|
+
viewBox="0 0 24 24"
|
|
43
|
+
fill="none"
|
|
44
|
+
stroke="currentColor"
|
|
45
|
+
strokeWidth="2"
|
|
46
|
+
strokeLinecap="round"
|
|
47
|
+
strokeLinejoin="round"
|
|
48
|
+
aria-hidden="true"
|
|
49
|
+
>
|
|
50
|
+
<line x1="3" y1="6" x2="21" y2="6" />
|
|
51
|
+
<line x1="3" y1="12" x2="21" y2="12" />
|
|
52
|
+
<line x1="3" y1="18" x2="21" y2="18" />
|
|
53
|
+
</svg>
|
|
54
|
+
</button>
|
|
55
|
+
|
|
56
|
+
{open ? (
|
|
57
|
+
<div className="fixed inset-0 z-50 sm:hidden">
|
|
58
|
+
<button
|
|
59
|
+
type="button"
|
|
60
|
+
onClick={() => setOpen(false)}
|
|
61
|
+
aria-label="Close menu"
|
|
62
|
+
className="absolute inset-0 bg-background/80 backdrop-blur-sm"
|
|
63
|
+
/>
|
|
64
|
+
<nav
|
|
65
|
+
id="mobile-nav-drawer"
|
|
66
|
+
aria-label="Mobile navigation"
|
|
67
|
+
className="absolute inset-y-0 right-0 w-[85%] max-w-sm flex flex-col bg-background border-l border-border shadow-2xl"
|
|
68
|
+
>
|
|
69
|
+
<div className="flex items-center justify-between px-6 py-4 border-b border-border">
|
|
70
|
+
<span className="text-xs font-semibold uppercase tracking-[0.16em] text-muted-foreground">
|
|
71
|
+
Menu
|
|
72
|
+
</span>
|
|
73
|
+
<button
|
|
74
|
+
type="button"
|
|
75
|
+
onClick={() => setOpen(false)}
|
|
76
|
+
aria-label="Close menu"
|
|
77
|
+
className="grid place-items-center w-11 h-11 -mr-2 rounded-md text-foreground hover:bg-muted transition-colors"
|
|
78
|
+
>
|
|
79
|
+
<svg
|
|
80
|
+
width="20"
|
|
81
|
+
height="20"
|
|
82
|
+
viewBox="0 0 24 24"
|
|
83
|
+
fill="none"
|
|
84
|
+
stroke="currentColor"
|
|
85
|
+
strokeWidth="2"
|
|
86
|
+
strokeLinecap="round"
|
|
87
|
+
strokeLinejoin="round"
|
|
88
|
+
aria-hidden="true"
|
|
89
|
+
>
|
|
90
|
+
<line x1="18" y1="6" x2="6" y2="18" />
|
|
91
|
+
<line x1="6" y1="6" x2="18" y2="18" />
|
|
92
|
+
</svg>
|
|
93
|
+
</button>
|
|
94
|
+
</div>
|
|
95
|
+
<ul className="flex flex-col gap-1 px-3 py-4">
|
|
96
|
+
{brand.header.nav.map((link) => (
|
|
97
|
+
<li key={link.href}>
|
|
98
|
+
<Link
|
|
99
|
+
href={link.href}
|
|
100
|
+
onClick={() => setOpen(false)}
|
|
101
|
+
className="block px-3 py-3 rounded-md text-base font-medium text-foreground hover:bg-muted transition-colors"
|
|
102
|
+
>
|
|
103
|
+
{link.label}
|
|
104
|
+
</Link>
|
|
105
|
+
</li>
|
|
106
|
+
))}
|
|
107
|
+
</ul>
|
|
108
|
+
</nav>
|
|
109
|
+
</div>
|
|
110
|
+
) : null}
|
|
111
|
+
</>
|
|
112
|
+
);
|
|
113
|
+
}
|
|
@@ -13,7 +13,7 @@ interface PolicyShape {
|
|
|
13
13
|
*/
|
|
14
14
|
export function PolicyPage({ policy }: { policy: PolicyShape }) {
|
|
15
15
|
return (
|
|
16
|
-
<article className="max-w-3xl mx-auto px-8 py-16 prose prose-lg max-w-none">
|
|
16
|
+
<article className="max-w-3xl mx-auto px-6 sm:px-8 py-16 prose prose-lg max-w-none">
|
|
17
17
|
<p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-primary mb-2 not-prose">
|
|
18
18
|
{policy.eyebrow}
|
|
19
19
|
</p>
|