@cimplify/cli 0.6.9 → 0.6.10
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-U4S5DBPN.mjs → add-GDHA7MKM.mjs} +1 -1
- package/dist/{chunk-CMF2X3SI.mjs → chunk-5L6LJE6I.mjs} +1 -1
- package/dist/{chunk-EALN6SAN.mjs → chunk-EKJ6T66O.mjs} +25 -25
- package/dist/{chunk-4HDXZJMR.mjs → chunk-ZTKQOLAC.mjs} +1 -1
- package/dist/dispatcher.mjs +9 -9
- package/dist/{doctor-BTMEATD4.mjs → doctor-SSSYBYCL.mjs} +2 -2
- package/dist/{explain-JH4TKGTP.mjs → explain-VG7XUP62.mjs} +1 -1
- package/dist/{introspect-DOZCE52F.mjs → introspect-HYJ6VI3U.mjs} +2 -2
- package/dist/{list-KO2QX42Q.mjs → list-NQP4SU5K.mjs} +1 -1
- package/dist/{update-O2UR6PAW.mjs → update-HHRCPKSU.mjs} +1 -1
- package/package.json +1 -1
- package/templates/storefront-auto/app/layout.tsx +3 -1
- package/templates/storefront-auto/components/product-modal.tsx +104 -0
- package/templates/storefront-auto/lib/site-url.ts +3 -26
- package/templates/storefront-auto/lib/site.config.ts +7 -0
- package/templates/storefront-auto/next.config.ts +7 -0
- package/templates/storefront-bakery/app/layout.tsx +3 -1
- package/templates/storefront-bakery/lib/site-url.ts +3 -26
- package/templates/storefront-bakery/lib/site.config.ts +7 -0
- package/templates/storefront-bakery/next.config.ts +7 -0
- package/templates/storefront-fashion/app/layout.tsx +3 -1
- package/templates/storefront-fashion/components/product-modal.tsx +104 -0
- package/templates/storefront-fashion/lib/site-url.ts +3 -26
- package/templates/storefront-fashion/lib/site.config.ts +7 -0
- package/templates/storefront-fashion/next.config.ts +7 -0
- package/templates/storefront-grocery/app/layout.tsx +3 -1
- package/templates/storefront-grocery/lib/site-url.ts +3 -26
- package/templates/storefront-grocery/lib/site.config.ts +7 -0
- package/templates/storefront-grocery/next.config.ts +7 -0
- package/templates/storefront-pharmacy/app/layout.tsx +3 -1
- package/templates/storefront-pharmacy/components/product-modal.tsx +104 -0
- package/templates/storefront-pharmacy/lib/site-url.ts +3 -26
- package/templates/storefront-pharmacy/lib/site.config.ts +7 -0
- package/templates/storefront-pharmacy/next.config.ts +7 -0
- package/templates/storefront-restaurant/app/layout.tsx +3 -1
- package/templates/storefront-restaurant/app/reservations/reservations-client.tsx +1 -1
- package/templates/storefront-restaurant/lib/site-url.ts +3 -26
- package/templates/storefront-restaurant/lib/site.config.ts +7 -0
- package/templates/storefront-restaurant/next.config.ts +7 -0
- package/templates/storefront-retail/app/layout.tsx +3 -1
- package/templates/storefront-retail/components/product-modal.tsx +104 -0
- package/templates/storefront-retail/lib/site-url.ts +3 -26
- package/templates/storefront-retail/lib/site.config.ts +7 -0
- package/templates/storefront-retail/next.config.ts +7 -0
- package/templates/storefront-services/app/book/book-client.tsx +2 -1
- package/templates/storefront-services/app/layout.tsx +3 -1
- package/templates/storefront-services/lib/site-url.ts +3 -26
- package/templates/storefront-services/lib/site.config.ts +7 -0
- package/templates/storefront-services/next.config.ts +7 -0
- package/templates/storefront-auto/app/login/page.tsx +0 -17
- package/templates/storefront-auto/app/signup/page.tsx +0 -17
- package/templates/storefront-bakery/app/login/page.tsx +0 -17
- package/templates/storefront-bakery/app/signup/page.tsx +0 -17
- package/templates/storefront-fashion/app/login/page.tsx +0 -17
- package/templates/storefront-fashion/app/signup/page.tsx +0 -17
- package/templates/storefront-grocery/app/login/page.tsx +0 -17
- package/templates/storefront-grocery/app/signup/page.tsx +0 -17
- package/templates/storefront-pharmacy/app/login/page.tsx +0 -17
- package/templates/storefront-pharmacy/app/signup/page.tsx +0 -17
- package/templates/storefront-restaurant/app/login/page.tsx +0 -17
- package/templates/storefront-restaurant/app/signup/page.tsx +0 -17
- package/templates/storefront-retail/app/login/page.tsx +0 -17
- package/templates/storefront-retail/app/signup/page.tsx +0 -17
- package/templates/storefront-services/app/login/page.tsx +0 -17
- package/templates/storefront-services/app/signup/page.tsx +0 -17
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useEffect } from "react";
|
|
4
|
+
import Image from "next/image";
|
|
5
|
+
import { useRouter, useSearchParams, usePathname } from "next/navigation";
|
|
6
|
+
import { ProductSheet, useProduct, useCart } from "@cimplify/sdk/react";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* URL-driven product modal. Reads `?product=<slug>` and renders the SDK's
|
|
10
|
+
* `<ProductSheet/>` — vertical layout with image-on-top, then header, then
|
|
11
|
+
* the variant/add-on/composite/bundle customizer. Closing the modal clears
|
|
12
|
+
* the search param. Deep-linkable and survives reloads.
|
|
13
|
+
*/
|
|
14
|
+
export function ProductModal() {
|
|
15
|
+
const router = useRouter();
|
|
16
|
+
const pathname = usePathname();
|
|
17
|
+
const searchParams = useSearchParams();
|
|
18
|
+
const slug = searchParams?.get("product") ?? null;
|
|
19
|
+
|
|
20
|
+
const { product } = useProduct(slug ?? "", { enabled: Boolean(slug) });
|
|
21
|
+
const { addItem } = useCart();
|
|
22
|
+
|
|
23
|
+
useEffect(() => {
|
|
24
|
+
if (!slug) return;
|
|
25
|
+
const original = document.body.style.overflow;
|
|
26
|
+
document.body.style.overflow = "hidden";
|
|
27
|
+
return () => {
|
|
28
|
+
document.body.style.overflow = original;
|
|
29
|
+
};
|
|
30
|
+
}, [slug]);
|
|
31
|
+
|
|
32
|
+
useEffect(() => {
|
|
33
|
+
if (!slug) return;
|
|
34
|
+
const onKey = (e: KeyboardEvent) => {
|
|
35
|
+
if (e.key === "Escape") close();
|
|
36
|
+
};
|
|
37
|
+
window.addEventListener("keydown", onKey);
|
|
38
|
+
return () => window.removeEventListener("keydown", onKey);
|
|
39
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
40
|
+
}, [slug]);
|
|
41
|
+
|
|
42
|
+
if (!slug) return null;
|
|
43
|
+
|
|
44
|
+
function close() {
|
|
45
|
+
const next = new URLSearchParams(searchParams?.toString() ?? "");
|
|
46
|
+
next.delete("product");
|
|
47
|
+
const qs = next.toString();
|
|
48
|
+
router.replace(qs ? `${pathname}?${qs}` : pathname, { scroll: false });
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return (
|
|
52
|
+
<div
|
|
53
|
+
role="dialog"
|
|
54
|
+
aria-modal="true"
|
|
55
|
+
onClick={close}
|
|
56
|
+
className="fixed inset-0 z-[100] flex items-end sm:items-center justify-center sm:p-6 bg-foreground/50 backdrop-blur-sm"
|
|
57
|
+
>
|
|
58
|
+
<div
|
|
59
|
+
onClick={(e) => e.stopPropagation()}
|
|
60
|
+
className="relative w-full sm:max-w-lg max-h-[92vh] overflow-auto bg-card sm:rounded-3xl rounded-t-3xl shadow-2xl"
|
|
61
|
+
>
|
|
62
|
+
<button
|
|
63
|
+
onClick={close}
|
|
64
|
+
aria-label="Close product details"
|
|
65
|
+
className="absolute top-4 right-4 z-10 w-9 h-9 rounded-full bg-card border border-border text-sm cursor-pointer transition-colors hover:bg-muted"
|
|
66
|
+
>
|
|
67
|
+
✕
|
|
68
|
+
</button>
|
|
69
|
+
{product ? (
|
|
70
|
+
<ProductSheet
|
|
71
|
+
product={product}
|
|
72
|
+
onClose={close}
|
|
73
|
+
onAddToCart={async (p, qty, options) => {
|
|
74
|
+
await addItem(p, qty, options);
|
|
75
|
+
close();
|
|
76
|
+
}}
|
|
77
|
+
renderImage={({ src, alt, className }) => (
|
|
78
|
+
<Image
|
|
79
|
+
src={src}
|
|
80
|
+
alt={alt}
|
|
81
|
+
width={1200}
|
|
82
|
+
height={900}
|
|
83
|
+
className={className}
|
|
84
|
+
style={{ width: "100%", height: "auto", objectFit: "cover" }}
|
|
85
|
+
priority
|
|
86
|
+
/>
|
|
87
|
+
)}
|
|
88
|
+
classNames={{
|
|
89
|
+
root: "p-6 sm:p-8 gap-4",
|
|
90
|
+
image: "rounded-2xl overflow-hidden -mx-6 sm:-mx-8 -mt-6 sm:-mt-8 mb-2",
|
|
91
|
+
header: "flex items-baseline justify-between gap-4",
|
|
92
|
+
name: "font-serif text-2xl font-semibold m-0",
|
|
93
|
+
price: "text-lg font-semibold text-primary",
|
|
94
|
+
description: "text-sm text-muted-foreground leading-relaxed",
|
|
95
|
+
customizer: "pt-2",
|
|
96
|
+
}}
|
|
97
|
+
/>
|
|
98
|
+
) : (
|
|
99
|
+
<div className="p-8 text-center text-muted-foreground">Loading…</div>
|
|
100
|
+
)}
|
|
101
|
+
</div>
|
|
102
|
+
</div>
|
|
103
|
+
);
|
|
104
|
+
}
|
|
@@ -1,29 +1,6 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { SITE_URL } from "./site.config";
|
|
2
2
|
|
|
3
|
-
/**
|
|
4
|
-
* Canonical absolute URL for this storefront, derived at request time.
|
|
5
|
-
*
|
|
6
|
-
* Resolution order:
|
|
7
|
-
* 1. `NEXT_PUBLIC_SITE_URL` — set when you need a fixed canonical
|
|
8
|
-
* (multiple domains, www-vs-apex preference, etc.).
|
|
9
|
-
* 2. The request `Host` header — works automatically on every deploy,
|
|
10
|
-
* including preview URLs and custom domains.
|
|
11
|
-
* 3. `https://example.com` — only ever returned during prerender of
|
|
12
|
-
* a page that has no live request (rare in App Router).
|
|
13
|
-
*/
|
|
3
|
+
/** Canonical absolute URL for this storefront. */
|
|
14
4
|
export async function getSiteUrl(): Promise<string> {
|
|
15
|
-
|
|
16
|
-
if (explicit) return explicit;
|
|
17
|
-
|
|
18
|
-
try {
|
|
19
|
-
const h = await headers();
|
|
20
|
-
const host = h.get("host");
|
|
21
|
-
if (host) {
|
|
22
|
-
const isLocal = host.startsWith("localhost") || host.startsWith("127.");
|
|
23
|
-
return `${isLocal ? "http" : "https"}://${host}`;
|
|
24
|
-
}
|
|
25
|
-
} catch {
|
|
26
|
-
// `headers()` is unavailable in some build contexts — fall through.
|
|
27
|
-
}
|
|
28
|
-
return "https://example.com";
|
|
5
|
+
return SITE_URL;
|
|
29
6
|
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Canonical absolute URL. The platform overwrites this at build time with the
|
|
3
|
+
* storefront's primary domain; the default only applies to local builds. It's
|
|
4
|
+
* per-deploy config, not per-request, so it stays a static constant — keeping
|
|
5
|
+
* the root layout prerenderable under `cacheComponents`.
|
|
6
|
+
*/
|
|
7
|
+
export const SITE_URL = "https://example.com";
|
|
@@ -24,6 +24,13 @@ const nextConfig: NextConfig = {
|
|
|
24
24
|
// `cacheTag` / `cacheLife` for SSR caching. Cached chrome streams first,
|
|
25
25
|
// dynamic Suspense boundaries fill in when ready.
|
|
26
26
|
cacheComponents: true,
|
|
27
|
+
async redirects() {
|
|
28
|
+
// Config-level so cacheComponents needn't prerender a redirect()-only page.
|
|
29
|
+
return [
|
|
30
|
+
{ source: "/login", destination: "/account", permanent: false },
|
|
31
|
+
{ source: "/signup", destination: "/account", permanent: false },
|
|
32
|
+
];
|
|
33
|
+
},
|
|
27
34
|
async rewrites() {
|
|
28
35
|
return [
|
|
29
36
|
{ source: "/api/v1/:path*", destination: `${STOREFRONT_URL}/api/v1/:path*` },
|
|
@@ -53,7 +53,9 @@ export default function RootLayout({ children }: { children: React.ReactNode })
|
|
|
53
53
|
</Suspense>
|
|
54
54
|
<Providers>
|
|
55
55
|
<Header />
|
|
56
|
-
<main className="flex-1 pb-12 w-full">
|
|
56
|
+
<main className="flex-1 pb-12 w-full">
|
|
57
|
+
<Suspense fallback={null}>{children}</Suspense>
|
|
58
|
+
</main>
|
|
57
59
|
<Footer />
|
|
58
60
|
<Suspense fallback={null}>
|
|
59
61
|
<ProductModal />
|
|
@@ -59,7 +59,7 @@ export function ReservationsClient({ options }: { options: Product[] }) {
|
|
|
59
59
|
try {
|
|
60
60
|
const when = new Date(selectedSlotKey).toLocaleString();
|
|
61
61
|
const note = `Party of ${partySize} · ${when}${notes ? ` · ${notes}` : ""}`;
|
|
62
|
-
await addItem(selectedOption, 1, {
|
|
62
|
+
await addItem(selectedOption, 1, { specialInstructions: note });
|
|
63
63
|
router.push("/checkout");
|
|
64
64
|
} catch {
|
|
65
65
|
setSubmitting(false);
|
|
@@ -1,29 +1,6 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { SITE_URL } from "./site.config";
|
|
2
2
|
|
|
3
|
-
/**
|
|
4
|
-
* Canonical absolute URL for this storefront, derived at request time.
|
|
5
|
-
*
|
|
6
|
-
* Resolution order:
|
|
7
|
-
* 1. `NEXT_PUBLIC_SITE_URL` — set when you need a fixed canonical
|
|
8
|
-
* (multiple domains, www-vs-apex preference, etc.).
|
|
9
|
-
* 2. The request `Host` header — works automatically on every deploy,
|
|
10
|
-
* including preview URLs and custom domains.
|
|
11
|
-
* 3. `https://example.com` — only ever returned during prerender of
|
|
12
|
-
* a page that has no live request (rare in App Router).
|
|
13
|
-
*/
|
|
3
|
+
/** Canonical absolute URL for this storefront. */
|
|
14
4
|
export async function getSiteUrl(): Promise<string> {
|
|
15
|
-
|
|
16
|
-
if (explicit) return explicit;
|
|
17
|
-
|
|
18
|
-
try {
|
|
19
|
-
const h = await headers();
|
|
20
|
-
const host = h.get("host");
|
|
21
|
-
if (host) {
|
|
22
|
-
const isLocal = host.startsWith("localhost") || host.startsWith("127.");
|
|
23
|
-
return `${isLocal ? "http" : "https"}://${host}`;
|
|
24
|
-
}
|
|
25
|
-
} catch {
|
|
26
|
-
// `headers()` is unavailable in some build contexts — fall through.
|
|
27
|
-
}
|
|
28
|
-
return "https://example.com";
|
|
5
|
+
return SITE_URL;
|
|
29
6
|
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Canonical absolute URL. The platform overwrites this at build time with the
|
|
3
|
+
* storefront's primary domain; the default only applies to local builds. It's
|
|
4
|
+
* per-deploy config, not per-request, so it stays a static constant — keeping
|
|
5
|
+
* the root layout prerenderable under `cacheComponents`.
|
|
6
|
+
*/
|
|
7
|
+
export const SITE_URL = "https://example.com";
|
|
@@ -24,6 +24,13 @@ const nextConfig: NextConfig = {
|
|
|
24
24
|
// `cacheTag` / `cacheLife` for SSR caching. Cached chrome streams first,
|
|
25
25
|
// dynamic Suspense boundaries fill in when ready.
|
|
26
26
|
cacheComponents: true,
|
|
27
|
+
async redirects() {
|
|
28
|
+
// Config-level so cacheComponents needn't prerender a redirect()-only page.
|
|
29
|
+
return [
|
|
30
|
+
{ source: "/login", destination: "/account", permanent: false },
|
|
31
|
+
{ source: "/signup", destination: "/account", permanent: false },
|
|
32
|
+
];
|
|
33
|
+
},
|
|
27
34
|
async rewrites() {
|
|
28
35
|
return [
|
|
29
36
|
{ source: "/api/v1/:path*", destination: `${STOREFRONT_URL}/api/v1/:path*` },
|
|
@@ -53,7 +53,9 @@ export default function RootLayout({ children }: { children: React.ReactNode })
|
|
|
53
53
|
</Suspense>
|
|
54
54
|
<Providers>
|
|
55
55
|
<Header />
|
|
56
|
-
<main className="flex-1 pb-12 w-full">
|
|
56
|
+
<main className="flex-1 pb-12 w-full">
|
|
57
|
+
<Suspense fallback={null}>{children}</Suspense>
|
|
58
|
+
</main>
|
|
57
59
|
<Footer />
|
|
58
60
|
<Suspense fallback={null}>
|
|
59
61
|
<ProductModal />
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useEffect } from "react";
|
|
4
|
+
import Image from "next/image";
|
|
5
|
+
import { useRouter, useSearchParams, usePathname } from "next/navigation";
|
|
6
|
+
import { ProductSheet, useProduct, useCart } from "@cimplify/sdk/react";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* URL-driven product modal. Reads `?product=<slug>` and renders the SDK's
|
|
10
|
+
* `<ProductSheet/>` — vertical layout with image-on-top, then header, then
|
|
11
|
+
* the variant/add-on/composite/bundle customizer. Closing the modal clears
|
|
12
|
+
* the search param. Deep-linkable and survives reloads.
|
|
13
|
+
*/
|
|
14
|
+
export function ProductModal() {
|
|
15
|
+
const router = useRouter();
|
|
16
|
+
const pathname = usePathname();
|
|
17
|
+
const searchParams = useSearchParams();
|
|
18
|
+
const slug = searchParams?.get("product") ?? null;
|
|
19
|
+
|
|
20
|
+
const { product } = useProduct(slug ?? "", { enabled: Boolean(slug) });
|
|
21
|
+
const { addItem } = useCart();
|
|
22
|
+
|
|
23
|
+
useEffect(() => {
|
|
24
|
+
if (!slug) return;
|
|
25
|
+
const original = document.body.style.overflow;
|
|
26
|
+
document.body.style.overflow = "hidden";
|
|
27
|
+
return () => {
|
|
28
|
+
document.body.style.overflow = original;
|
|
29
|
+
};
|
|
30
|
+
}, [slug]);
|
|
31
|
+
|
|
32
|
+
useEffect(() => {
|
|
33
|
+
if (!slug) return;
|
|
34
|
+
const onKey = (e: KeyboardEvent) => {
|
|
35
|
+
if (e.key === "Escape") close();
|
|
36
|
+
};
|
|
37
|
+
window.addEventListener("keydown", onKey);
|
|
38
|
+
return () => window.removeEventListener("keydown", onKey);
|
|
39
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
40
|
+
}, [slug]);
|
|
41
|
+
|
|
42
|
+
if (!slug) return null;
|
|
43
|
+
|
|
44
|
+
function close() {
|
|
45
|
+
const next = new URLSearchParams(searchParams?.toString() ?? "");
|
|
46
|
+
next.delete("product");
|
|
47
|
+
const qs = next.toString();
|
|
48
|
+
router.replace(qs ? `${pathname}?${qs}` : pathname, { scroll: false });
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return (
|
|
52
|
+
<div
|
|
53
|
+
role="dialog"
|
|
54
|
+
aria-modal="true"
|
|
55
|
+
onClick={close}
|
|
56
|
+
className="fixed inset-0 z-[100] flex items-end sm:items-center justify-center sm:p-6 bg-foreground/50 backdrop-blur-sm"
|
|
57
|
+
>
|
|
58
|
+
<div
|
|
59
|
+
onClick={(e) => e.stopPropagation()}
|
|
60
|
+
className="relative w-full sm:max-w-lg max-h-[92vh] overflow-auto bg-card sm:rounded-3xl rounded-t-3xl shadow-2xl"
|
|
61
|
+
>
|
|
62
|
+
<button
|
|
63
|
+
onClick={close}
|
|
64
|
+
aria-label="Close product details"
|
|
65
|
+
className="absolute top-4 right-4 z-10 w-9 h-9 rounded-full bg-card border border-border text-sm cursor-pointer transition-colors hover:bg-muted"
|
|
66
|
+
>
|
|
67
|
+
✕
|
|
68
|
+
</button>
|
|
69
|
+
{product ? (
|
|
70
|
+
<ProductSheet
|
|
71
|
+
product={product}
|
|
72
|
+
onClose={close}
|
|
73
|
+
onAddToCart={async (p, qty, options) => {
|
|
74
|
+
await addItem(p, qty, options);
|
|
75
|
+
close();
|
|
76
|
+
}}
|
|
77
|
+
renderImage={({ src, alt, className }) => (
|
|
78
|
+
<Image
|
|
79
|
+
src={src}
|
|
80
|
+
alt={alt}
|
|
81
|
+
width={1200}
|
|
82
|
+
height={900}
|
|
83
|
+
className={className}
|
|
84
|
+
style={{ width: "100%", height: "auto", objectFit: "cover" }}
|
|
85
|
+
priority
|
|
86
|
+
/>
|
|
87
|
+
)}
|
|
88
|
+
classNames={{
|
|
89
|
+
root: "p-6 sm:p-8 gap-4",
|
|
90
|
+
image: "rounded-2xl overflow-hidden -mx-6 sm:-mx-8 -mt-6 sm:-mt-8 mb-2",
|
|
91
|
+
header: "flex items-baseline justify-between gap-4",
|
|
92
|
+
name: "font-serif text-2xl font-semibold m-0",
|
|
93
|
+
price: "text-lg font-semibold text-primary",
|
|
94
|
+
description: "text-sm text-muted-foreground leading-relaxed",
|
|
95
|
+
customizer: "pt-2",
|
|
96
|
+
}}
|
|
97
|
+
/>
|
|
98
|
+
) : (
|
|
99
|
+
<div className="p-8 text-center text-muted-foreground">Loading…</div>
|
|
100
|
+
)}
|
|
101
|
+
</div>
|
|
102
|
+
</div>
|
|
103
|
+
);
|
|
104
|
+
}
|
|
@@ -1,29 +1,6 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { SITE_URL } from "./site.config";
|
|
2
2
|
|
|
3
|
-
/**
|
|
4
|
-
* Canonical absolute URL for this storefront, derived at request time.
|
|
5
|
-
*
|
|
6
|
-
* Resolution order:
|
|
7
|
-
* 1. `NEXT_PUBLIC_SITE_URL` — set when you need a fixed canonical
|
|
8
|
-
* (multiple domains, www-vs-apex preference, etc.).
|
|
9
|
-
* 2. The request `Host` header — works automatically on every deploy,
|
|
10
|
-
* including preview URLs and custom domains.
|
|
11
|
-
* 3. `https://example.com` — only ever returned during prerender of
|
|
12
|
-
* a page that has no live request (rare in App Router).
|
|
13
|
-
*/
|
|
3
|
+
/** Canonical absolute URL for this storefront. */
|
|
14
4
|
export async function getSiteUrl(): Promise<string> {
|
|
15
|
-
|
|
16
|
-
if (explicit) return explicit;
|
|
17
|
-
|
|
18
|
-
try {
|
|
19
|
-
const h = await headers();
|
|
20
|
-
const host = h.get("host");
|
|
21
|
-
if (host) {
|
|
22
|
-
const isLocal = host.startsWith("localhost") || host.startsWith("127.");
|
|
23
|
-
return `${isLocal ? "http" : "https"}://${host}`;
|
|
24
|
-
}
|
|
25
|
-
} catch {
|
|
26
|
-
// `headers()` is unavailable in some build contexts — fall through.
|
|
27
|
-
}
|
|
28
|
-
return "https://example.com";
|
|
5
|
+
return SITE_URL;
|
|
29
6
|
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Canonical absolute URL. The platform overwrites this at build time with the
|
|
3
|
+
* storefront's primary domain; the default only applies to local builds. It's
|
|
4
|
+
* per-deploy config, not per-request, so it stays a static constant — keeping
|
|
5
|
+
* the root layout prerenderable under `cacheComponents`.
|
|
6
|
+
*/
|
|
7
|
+
export const SITE_URL = "https://example.com";
|
|
@@ -24,6 +24,13 @@ const nextConfig: NextConfig = {
|
|
|
24
24
|
// `cacheTag` / `cacheLife` for SSR caching. Cached chrome streams first,
|
|
25
25
|
// dynamic Suspense boundaries fill in when ready.
|
|
26
26
|
cacheComponents: true,
|
|
27
|
+
async redirects() {
|
|
28
|
+
// Config-level so cacheComponents needn't prerender a redirect()-only page.
|
|
29
|
+
return [
|
|
30
|
+
{ source: "/login", destination: "/account", permanent: false },
|
|
31
|
+
{ source: "/signup", destination: "/account", permanent: false },
|
|
32
|
+
];
|
|
33
|
+
},
|
|
27
34
|
async rewrites() {
|
|
28
35
|
return [
|
|
29
36
|
{ source: "/api/v1/:path*", destination: `${STOREFRONT_URL}/api/v1/:path*` },
|
|
@@ -5,6 +5,7 @@ import { useRouter } from "next/navigation";
|
|
|
5
5
|
import type { Product } from "@cimplify/sdk";
|
|
6
6
|
import type { AvailableSlot } from "@cimplify/sdk";
|
|
7
7
|
import { useCart, DateSlotPicker } from "@cimplify/sdk/react";
|
|
8
|
+
import { brand } from "@/lib/brand";
|
|
8
9
|
|
|
9
10
|
/**
|
|
10
11
|
* Booking flow:
|
|
@@ -82,7 +83,7 @@ export function BookClient({ treatments }: { treatments: Product[] }) {
|
|
|
82
83
|
<p className="font-semibold text-sm m-0 truncate">{t.name}</p>
|
|
83
84
|
<p className="text-xs text-muted-foreground m-0">
|
|
84
85
|
{t.duration_minutes ? `${t.duration_minutes} min · ` : ""}
|
|
85
|
-
{
|
|
86
|
+
{brand.currency} {t.default_price}
|
|
86
87
|
</p>
|
|
87
88
|
</div>
|
|
88
89
|
{active && (
|
|
@@ -53,7 +53,9 @@ export default function RootLayout({ children }: { children: React.ReactNode })
|
|
|
53
53
|
</Suspense>
|
|
54
54
|
<Providers>
|
|
55
55
|
<Header />
|
|
56
|
-
<main className="flex-1 pb-12 w-full">
|
|
56
|
+
<main className="flex-1 pb-12 w-full">
|
|
57
|
+
<Suspense fallback={null}>{children}</Suspense>
|
|
58
|
+
</main>
|
|
57
59
|
<Footer />
|
|
58
60
|
<Suspense fallback={null}>
|
|
59
61
|
<ProductModal />
|
|
@@ -1,29 +1,6 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { SITE_URL } from "./site.config";
|
|
2
2
|
|
|
3
|
-
/**
|
|
4
|
-
* Canonical absolute URL for this storefront, derived at request time.
|
|
5
|
-
*
|
|
6
|
-
* Resolution order:
|
|
7
|
-
* 1. `NEXT_PUBLIC_SITE_URL` — set when you need a fixed canonical
|
|
8
|
-
* (multiple domains, www-vs-apex preference, etc.).
|
|
9
|
-
* 2. The request `Host` header — works automatically on every deploy,
|
|
10
|
-
* including preview URLs and custom domains.
|
|
11
|
-
* 3. `https://example.com` — only ever returned during prerender of
|
|
12
|
-
* a page that has no live request (rare in App Router).
|
|
13
|
-
*/
|
|
3
|
+
/** Canonical absolute URL for this storefront. */
|
|
14
4
|
export async function getSiteUrl(): Promise<string> {
|
|
15
|
-
|
|
16
|
-
if (explicit) return explicit;
|
|
17
|
-
|
|
18
|
-
try {
|
|
19
|
-
const h = await headers();
|
|
20
|
-
const host = h.get("host");
|
|
21
|
-
if (host) {
|
|
22
|
-
const isLocal = host.startsWith("localhost") || host.startsWith("127.");
|
|
23
|
-
return `${isLocal ? "http" : "https"}://${host}`;
|
|
24
|
-
}
|
|
25
|
-
} catch {
|
|
26
|
-
// `headers()` is unavailable in some build contexts — fall through.
|
|
27
|
-
}
|
|
28
|
-
return "https://example.com";
|
|
5
|
+
return SITE_URL;
|
|
29
6
|
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Canonical absolute URL. The platform overwrites this at build time with the
|
|
3
|
+
* storefront's primary domain; the default only applies to local builds. It's
|
|
4
|
+
* per-deploy config, not per-request, so it stays a static constant — keeping
|
|
5
|
+
* the root layout prerenderable under `cacheComponents`.
|
|
6
|
+
*/
|
|
7
|
+
export const SITE_URL = "https://example.com";
|
|
@@ -24,6 +24,13 @@ const nextConfig: NextConfig = {
|
|
|
24
24
|
// `cacheTag` / `cacheLife` for SSR caching. Cached chrome streams first,
|
|
25
25
|
// dynamic Suspense boundaries fill in when ready.
|
|
26
26
|
cacheComponents: true,
|
|
27
|
+
async redirects() {
|
|
28
|
+
// Config-level so cacheComponents needn't prerender a redirect()-only page.
|
|
29
|
+
return [
|
|
30
|
+
{ source: "/login", destination: "/account", permanent: false },
|
|
31
|
+
{ source: "/signup", destination: "/account", permanent: false },
|
|
32
|
+
];
|
|
33
|
+
},
|
|
27
34
|
async rewrites() {
|
|
28
35
|
return [
|
|
29
36
|
{ source: "/api/v1/:path*", destination: `${STOREFRONT_URL}/api/v1/:path*` },
|
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
import type { Metadata } from "next";
|
|
2
|
-
import { redirect } from "next/navigation";
|
|
3
|
-
import { brand } from "@/lib/brand";
|
|
4
|
-
|
|
5
|
-
export const metadata: Metadata = {
|
|
6
|
-
title: `Sign in — ${brand.name}`,
|
|
7
|
-
description: brand.account.loginSubtitle,
|
|
8
|
-
};
|
|
9
|
-
|
|
10
|
-
/**
|
|
11
|
-
* Cimplify Link (the iframe in `<CimplifyAccount>`) handles sign-in
|
|
12
|
-
* automatically when no session exists. We just bounce /login to /account
|
|
13
|
-
* so consumers landing here get the right UI without a duplicate form.
|
|
14
|
-
*/
|
|
15
|
-
export default function LoginPage(): never {
|
|
16
|
-
redirect("/account");
|
|
17
|
-
}
|
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
import type { Metadata } from "next";
|
|
2
|
-
import { redirect } from "next/navigation";
|
|
3
|
-
import { brand } from "@/lib/brand";
|
|
4
|
-
|
|
5
|
-
export const metadata: Metadata = {
|
|
6
|
-
title: `Create an account — ${brand.name}`,
|
|
7
|
-
description: brand.account.signupSubtitle,
|
|
8
|
-
};
|
|
9
|
-
|
|
10
|
-
/**
|
|
11
|
-
* Cimplify Link handles sign-up inside the `<CimplifyAccount>` iframe.
|
|
12
|
-
* We bounce /signup to /account; the iframe shows the create-account UI
|
|
13
|
-
* for visitors with no session.
|
|
14
|
-
*/
|
|
15
|
-
export default function SignupPage(): never {
|
|
16
|
-
redirect("/account");
|
|
17
|
-
}
|
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
import type { Metadata } from "next";
|
|
2
|
-
import { redirect } from "next/navigation";
|
|
3
|
-
import { brand } from "@/lib/brand";
|
|
4
|
-
|
|
5
|
-
export const metadata: Metadata = {
|
|
6
|
-
title: `Sign in — ${brand.name}`,
|
|
7
|
-
description: brand.account.loginSubtitle,
|
|
8
|
-
};
|
|
9
|
-
|
|
10
|
-
/**
|
|
11
|
-
* Cimplify Link (the iframe in `<CimplifyAccount>`) handles sign-in
|
|
12
|
-
* automatically when no session exists. We just bounce /login to /account
|
|
13
|
-
* so consumers landing here get the right UI without a duplicate form.
|
|
14
|
-
*/
|
|
15
|
-
export default function LoginPage(): never {
|
|
16
|
-
redirect("/account");
|
|
17
|
-
}
|
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
import type { Metadata } from "next";
|
|
2
|
-
import { redirect } from "next/navigation";
|
|
3
|
-
import { brand } from "@/lib/brand";
|
|
4
|
-
|
|
5
|
-
export const metadata: Metadata = {
|
|
6
|
-
title: `Create an account — ${brand.name}`,
|
|
7
|
-
description: brand.account.signupSubtitle,
|
|
8
|
-
};
|
|
9
|
-
|
|
10
|
-
/**
|
|
11
|
-
* Cimplify Link handles sign-up inside the `<CimplifyAccount>` iframe.
|
|
12
|
-
* We bounce /signup to /account; the iframe shows the create-account UI
|
|
13
|
-
* for visitors with no session.
|
|
14
|
-
*/
|
|
15
|
-
export default function SignupPage(): never {
|
|
16
|
-
redirect("/account");
|
|
17
|
-
}
|
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
import type { Metadata } from "next";
|
|
2
|
-
import { redirect } from "next/navigation";
|
|
3
|
-
import { brand } from "@/lib/brand";
|
|
4
|
-
|
|
5
|
-
export const metadata: Metadata = {
|
|
6
|
-
title: `Sign in — ${brand.name}`,
|
|
7
|
-
description: brand.account.loginSubtitle,
|
|
8
|
-
};
|
|
9
|
-
|
|
10
|
-
/**
|
|
11
|
-
* Cimplify Link (the iframe in `<CimplifyAccount>`) handles sign-in
|
|
12
|
-
* automatically when no session exists. We just bounce /login to /account
|
|
13
|
-
* so consumers landing here get the right UI without a duplicate form.
|
|
14
|
-
*/
|
|
15
|
-
export default function LoginPage(): never {
|
|
16
|
-
redirect("/account");
|
|
17
|
-
}
|
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
import type { Metadata } from "next";
|
|
2
|
-
import { redirect } from "next/navigation";
|
|
3
|
-
import { brand } from "@/lib/brand";
|
|
4
|
-
|
|
5
|
-
export const metadata: Metadata = {
|
|
6
|
-
title: `Create an account — ${brand.name}`,
|
|
7
|
-
description: brand.account.signupSubtitle,
|
|
8
|
-
};
|
|
9
|
-
|
|
10
|
-
/**
|
|
11
|
-
* Cimplify Link handles sign-up inside the `<CimplifyAccount>` iframe.
|
|
12
|
-
* We bounce /signup to /account; the iframe shows the create-account UI
|
|
13
|
-
* for visitors with no session.
|
|
14
|
-
*/
|
|
15
|
-
export default function SignupPage(): never {
|
|
16
|
-
redirect("/account");
|
|
17
|
-
}
|
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
import type { Metadata } from "next";
|
|
2
|
-
import { redirect } from "next/navigation";
|
|
3
|
-
import { brand } from "@/lib/brand";
|
|
4
|
-
|
|
5
|
-
export const metadata: Metadata = {
|
|
6
|
-
title: `Sign in — ${brand.name}`,
|
|
7
|
-
description: brand.account.loginSubtitle,
|
|
8
|
-
};
|
|
9
|
-
|
|
10
|
-
/**
|
|
11
|
-
* Cimplify Link (the iframe in `<CimplifyAccount>`) handles sign-in
|
|
12
|
-
* automatically when no session exists. We just bounce /login to /account
|
|
13
|
-
* so consumers landing here get the right UI without a duplicate form.
|
|
14
|
-
*/
|
|
15
|
-
export default function LoginPage(): never {
|
|
16
|
-
redirect("/account");
|
|
17
|
-
}
|
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
import type { Metadata } from "next";
|
|
2
|
-
import { redirect } from "next/navigation";
|
|
3
|
-
import { brand } from "@/lib/brand";
|
|
4
|
-
|
|
5
|
-
export const metadata: Metadata = {
|
|
6
|
-
title: `Create an account — ${brand.name}`,
|
|
7
|
-
description: brand.account.signupSubtitle,
|
|
8
|
-
};
|
|
9
|
-
|
|
10
|
-
/**
|
|
11
|
-
* Cimplify Link handles sign-up inside the `<CimplifyAccount>` iframe.
|
|
12
|
-
* We bounce /signup to /account; the iframe shows the create-account UI
|
|
13
|
-
* for visitors with no session.
|
|
14
|
-
*/
|
|
15
|
-
export default function SignupPage(): never {
|
|
16
|
-
redirect("/account");
|
|
17
|
-
}
|