@cimplify/sdk 0.41.0 → 0.42.0
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/cli/commands/deploy.mjs +561 -0
- package/dist/cli/commands/dev.mjs +434 -0
- package/dist/cli/commands/domains.mjs +479 -0
- package/dist/cli/commands/env.mjs +610 -0
- package/dist/cli/commands/link.mjs +325 -0
- package/dist/cli/commands/login.mjs +515 -0
- package/dist/cli/commands/logout.mjs +54 -0
- package/dist/cli/commands/logs.mjs +358 -0
- package/dist/cli/commands/projects.mjs +327 -0
- package/dist/cli/commands/rollback.mjs +477 -0
- package/dist/cli/commands/status.mjs +380 -0
- package/dist/cli/commands/unlink.mjs +58 -0
- package/dist/cli/commands/whoami.mjs +234 -0
- package/dist/dispatcher.mjs +91 -0
- package/dist/styles.css +1 -1
- package/package.json +1 -1
- package/templates/storefront-bakery/app/accessibility/page.tsx +11 -0
- package/templates/storefront-bakery/app/account/addresses/page.tsx +21 -0
- package/templates/storefront-bakery/app/account/orders/page.tsx +21 -0
- package/templates/storefront-bakery/app/account/page.tsx +22 -0
- package/templates/storefront-bakery/app/account/settings/page.tsx +21 -0
- package/templates/storefront-bakery/app/contact/contact-form.tsx +109 -0
- package/templates/storefront-bakery/app/contact/page.tsx +54 -0
- package/templates/storefront-bakery/app/login/page.tsx +17 -0
- package/templates/storefront-bakery/app/opensearch.xml/route.ts +37 -0
- package/templates/storefront-bakery/app/returns/page.tsx +11 -0
- package/templates/storefront-bakery/app/search/page.tsx +38 -0
- package/templates/storefront-bakery/app/search/search-client.tsx +7 -0
- package/templates/storefront-bakery/app/shipping/page.tsx +16 -0
- package/templates/storefront-bakery/app/signup/page.tsx +17 -0
- package/templates/storefront-bakery/app/sitemap-page/page.tsx +167 -0
- package/templates/storefront-bakery/app/track-order/page.tsx +24 -0
- package/templates/storefront-bakery/app/track-order/track-order-form.tsx +69 -0
- package/templates/storefront-bakery/components/account-iframe.tsx +13 -0
- package/templates/storefront-bakery/components/policy-page.tsx +49 -0
- package/templates/storefront-bakery/lib/brand.ts +119 -4
- package/templates/storefront-fashion/app/accessibility/page.tsx +11 -0
- package/templates/storefront-fashion/app/account/addresses/page.tsx +21 -0
- package/templates/storefront-fashion/app/account/orders/page.tsx +21 -0
- package/templates/storefront-fashion/app/account/page.tsx +22 -0
- package/templates/storefront-fashion/app/account/settings/page.tsx +21 -0
- package/templates/storefront-fashion/app/contact/contact-form.tsx +109 -0
- package/templates/storefront-fashion/app/contact/page.tsx +54 -0
- package/templates/storefront-fashion/app/login/page.tsx +17 -0
- package/templates/storefront-fashion/app/lookbook/page.tsx +132 -0
- package/templates/storefront-fashion/app/opensearch.xml/route.ts +37 -0
- package/templates/storefront-fashion/app/returns/page.tsx +11 -0
- package/templates/storefront-fashion/app/search/page.tsx +38 -0
- package/templates/storefront-fashion/app/search/search-client.tsx +7 -0
- package/templates/storefront-fashion/app/shipping/page.tsx +16 -0
- package/templates/storefront-fashion/app/signup/page.tsx +17 -0
- package/templates/storefront-fashion/app/sitemap-page/page.tsx +167 -0
- package/templates/storefront-fashion/app/size-guide/page.tsx +155 -0
- package/templates/storefront-fashion/app/track-order/page.tsx +24 -0
- package/templates/storefront-fashion/app/track-order/track-order-form.tsx +69 -0
- package/templates/storefront-fashion/components/account-iframe.tsx +13 -0
- package/templates/storefront-fashion/components/policy-page.tsx +49 -0
- package/templates/storefront-fashion/lib/brand.ts +99 -4
- package/templates/storefront-grocery/app/accessibility/page.tsx +11 -0
- package/templates/storefront-grocery/app/account/addresses/page.tsx +21 -0
- package/templates/storefront-grocery/app/account/orders/page.tsx +21 -0
- package/templates/storefront-grocery/app/account/page.tsx +22 -0
- package/templates/storefront-grocery/app/account/settings/page.tsx +21 -0
- package/templates/storefront-grocery/app/contact/contact-form.tsx +109 -0
- package/templates/storefront-grocery/app/contact/page.tsx +54 -0
- package/templates/storefront-grocery/app/login/page.tsx +17 -0
- package/templates/storefront-grocery/app/opensearch.xml/route.ts +37 -0
- package/templates/storefront-grocery/app/returns/page.tsx +11 -0
- package/templates/storefront-grocery/app/search/page.tsx +38 -0
- package/templates/storefront-grocery/app/search/search-client.tsx +7 -0
- package/templates/storefront-grocery/app/shipping/page.tsx +16 -0
- package/templates/storefront-grocery/app/signup/page.tsx +17 -0
- package/templates/storefront-grocery/app/sitemap-page/page.tsx +167 -0
- package/templates/storefront-grocery/app/track-order/page.tsx +24 -0
- package/templates/storefront-grocery/app/track-order/track-order-form.tsx +69 -0
- package/templates/storefront-grocery/components/account-iframe.tsx +13 -0
- package/templates/storefront-grocery/components/policy-page.tsx +49 -0
- package/templates/storefront-grocery/lib/brand.ts +92 -3
- package/templates/storefront-restaurant/app/accessibility/page.tsx +11 -0
- package/templates/storefront-restaurant/app/account/addresses/page.tsx +21 -0
- package/templates/storefront-restaurant/app/account/orders/page.tsx +21 -0
- package/templates/storefront-restaurant/app/account/page.tsx +22 -0
- package/templates/storefront-restaurant/app/account/settings/page.tsx +21 -0
- package/templates/storefront-restaurant/app/contact/contact-form.tsx +109 -0
- package/templates/storefront-restaurant/app/contact/page.tsx +54 -0
- package/templates/storefront-restaurant/app/login/page.tsx +17 -0
- package/templates/storefront-restaurant/app/opensearch.xml/route.ts +37 -0
- package/templates/storefront-restaurant/app/reservations/page.tsx +66 -0
- package/templates/storefront-restaurant/app/reservations/reservations-client.tsx +234 -0
- package/templates/storefront-restaurant/app/returns/page.tsx +11 -0
- package/templates/storefront-restaurant/app/search/page.tsx +38 -0
- package/templates/storefront-restaurant/app/search/search-client.tsx +7 -0
- package/templates/storefront-restaurant/app/shipping/page.tsx +16 -0
- package/templates/storefront-restaurant/app/signup/page.tsx +17 -0
- package/templates/storefront-restaurant/app/sitemap-page/page.tsx +167 -0
- package/templates/storefront-restaurant/app/track-order/page.tsx +24 -0
- package/templates/storefront-restaurant/app/track-order/track-order-form.tsx +69 -0
- package/templates/storefront-restaurant/components/account-iframe.tsx +13 -0
- package/templates/storefront-restaurant/components/policy-page.tsx +49 -0
- package/templates/storefront-restaurant/lib/brand.ts +91 -3
- package/templates/storefront-retail/app/accessibility/page.tsx +11 -0
- package/templates/storefront-retail/app/account/addresses/page.tsx +21 -0
- package/templates/storefront-retail/app/account/orders/page.tsx +21 -0
- package/templates/storefront-retail/app/account/page.tsx +22 -0
- package/templates/storefront-retail/app/account/settings/page.tsx +21 -0
- package/templates/storefront-retail/app/contact/contact-form.tsx +109 -0
- package/templates/storefront-retail/app/contact/page.tsx +54 -0
- package/templates/storefront-retail/app/login/page.tsx +17 -0
- package/templates/storefront-retail/app/opensearch.xml/route.ts +37 -0
- package/templates/storefront-retail/app/returns/page.tsx +11 -0
- package/templates/storefront-retail/app/search/page.tsx +38 -0
- package/templates/storefront-retail/app/search/search-client.tsx +7 -0
- package/templates/storefront-retail/app/shipping/page.tsx +16 -0
- package/templates/storefront-retail/app/signup/page.tsx +17 -0
- package/templates/storefront-retail/app/sitemap-page/page.tsx +167 -0
- package/templates/storefront-retail/app/track-order/page.tsx +24 -0
- package/templates/storefront-retail/app/track-order/track-order-form.tsx +69 -0
- package/templates/storefront-retail/components/account-iframe.tsx +13 -0
- package/templates/storefront-retail/components/policy-page.tsx +49 -0
- package/templates/storefront-retail/lib/brand.ts +105 -9
- package/templates/storefront-services/app/accessibility/page.tsx +11 -0
- package/templates/storefront-services/app/account/addresses/page.tsx +21 -0
- package/templates/storefront-services/app/account/orders/page.tsx +21 -0
- package/templates/storefront-services/app/account/page.tsx +22 -0
- package/templates/storefront-services/app/account/settings/page.tsx +21 -0
- package/templates/storefront-services/app/book/book-client.tsx +195 -0
- package/templates/storefront-services/app/book/page.tsx +65 -0
- package/templates/storefront-services/app/contact/contact-form.tsx +109 -0
- package/templates/storefront-services/app/contact/page.tsx +54 -0
- package/templates/storefront-services/app/login/page.tsx +17 -0
- package/templates/storefront-services/app/opensearch.xml/route.ts +37 -0
- package/templates/storefront-services/app/returns/page.tsx +11 -0
- package/templates/storefront-services/app/search/page.tsx +38 -0
- package/templates/storefront-services/app/search/search-client.tsx +7 -0
- package/templates/storefront-services/app/shipping/page.tsx +16 -0
- package/templates/storefront-services/app/signup/page.tsx +17 -0
- package/templates/storefront-services/app/sitemap-page/page.tsx +167 -0
- package/templates/storefront-services/app/track-order/page.tsx +24 -0
- package/templates/storefront-services/app/track-order/track-order-form.tsx +69 -0
- package/templates/storefront-services/components/account-iframe.tsx +13 -0
- package/templates/storefront-services/components/policy-page.tsx +49 -0
- package/templates/storefront-services/lib/brand.ts +93 -2
|
@@ -0,0 +1,17 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { brand } from "@/lib/brand";
|
|
2
|
+
|
|
3
|
+
const SITE_URL =
|
|
4
|
+
process.env.NEXT_PUBLIC_SITE_URL?.trim() || "https://example.com";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* OpenSearch description document — lets browsers add this site to the
|
|
8
|
+
* address bar's search engine list. When users press Tab after typing the
|
|
9
|
+
* domain, they get an inline search box that hits /search?q=...
|
|
10
|
+
*/
|
|
11
|
+
export async function GET(): Promise<Response> {
|
|
12
|
+
const xml = `<?xml version="1.0" encoding="UTF-8"?>
|
|
13
|
+
<OpenSearchDescription xmlns="http://a9.com/-/spec/opensearch/1.1/">
|
|
14
|
+
<ShortName>${escapeXml(brand.shortName)}</ShortName>
|
|
15
|
+
<Description>Search ${escapeXml(brand.name)}</Description>
|
|
16
|
+
<InputEncoding>UTF-8</InputEncoding>
|
|
17
|
+
<Url type="text/html" method="get" template="${SITE_URL}/search?q={searchTerms}" />
|
|
18
|
+
<Url type="application/opensearchdescription+xml" rel="self" template="${SITE_URL}/opensearch.xml" />
|
|
19
|
+
<moz:SearchForm>${SITE_URL}/search</moz:SearchForm>
|
|
20
|
+
</OpenSearchDescription>
|
|
21
|
+
`;
|
|
22
|
+
return new Response(xml, {
|
|
23
|
+
headers: {
|
|
24
|
+
"Content-Type": "application/opensearchdescription+xml; charset=utf-8",
|
|
25
|
+
"Cache-Control": "public, max-age=86400",
|
|
26
|
+
},
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function escapeXml(s: string): string {
|
|
31
|
+
return s
|
|
32
|
+
.replace(/&/g, "&")
|
|
33
|
+
.replace(/</g, "<")
|
|
34
|
+
.replace(/>/g, ">")
|
|
35
|
+
.replace(/"/g, """)
|
|
36
|
+
.replace(/'/g, "'");
|
|
37
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { Metadata } from "next";
|
|
2
|
+
import { brand } from "@/lib/brand";
|
|
3
|
+
import { PolicyPage } from "@/components/policy-page";
|
|
4
|
+
|
|
5
|
+
export const metadata: Metadata = {
|
|
6
|
+
title: `${brand.returns.title} — ${brand.name}`,
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
export default function ReturnsPage() {
|
|
10
|
+
return <PolicyPage policy={brand.returns} />;
|
|
11
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import type { Metadata } from "next";
|
|
2
|
+
import { Suspense } from "react";
|
|
3
|
+
import { SearchClient } from "./search-client";
|
|
4
|
+
import { brand } from "@/lib/brand";
|
|
5
|
+
|
|
6
|
+
export const metadata: Metadata = {
|
|
7
|
+
title: `Search — ${brand.name}`,
|
|
8
|
+
description: `Search ${brand.name} — products, collections, categories.`,
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export default function SearchPage() {
|
|
12
|
+
return (
|
|
13
|
+
<article className="max-w-7xl mx-auto px-6 sm:px-8 py-10">
|
|
14
|
+
<p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-primary mb-2">
|
|
15
|
+
Search
|
|
16
|
+
</p>
|
|
17
|
+
<h1 className="font-serif text-[clamp(2rem,4vw,2.75rem)] font-semibold mb-8 -tracking-[0.02em]">
|
|
18
|
+
Find anything.
|
|
19
|
+
</h1>
|
|
20
|
+
<Suspense fallback={<SearchSkeleton />}>
|
|
21
|
+
<SearchClient />
|
|
22
|
+
</Suspense>
|
|
23
|
+
</article>
|
|
24
|
+
);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function SearchSkeleton() {
|
|
28
|
+
return (
|
|
29
|
+
<div>
|
|
30
|
+
<div className="h-12 w-full max-w-xl bg-muted rounded animate-pulse mb-8" />
|
|
31
|
+
<div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-3 sm:gap-4">
|
|
32
|
+
{Array.from({ length: 8 }).map((_, i) => (
|
|
33
|
+
<div key={i} className="aspect-[4/3] bg-muted rounded-2xl animate-pulse" />
|
|
34
|
+
))}
|
|
35
|
+
</div>
|
|
36
|
+
</div>
|
|
37
|
+
);
|
|
38
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { Metadata } from "next";
|
|
2
|
+
import { brand } from "@/lib/brand";
|
|
3
|
+
import { PolicyPage } from "@/components/policy-page";
|
|
4
|
+
|
|
5
|
+
export const metadata: Metadata = {
|
|
6
|
+
title: `${brand.shipping.title} — ${brand.name}`,
|
|
7
|
+
description: brand.shipping.sections[0]?.body
|
|
8
|
+
? typeof brand.shipping.sections[0].body === "string"
|
|
9
|
+
? brand.shipping.sections[0].body
|
|
10
|
+
: brand.shipping.sections[0].body.intro
|
|
11
|
+
: undefined,
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export default function ShippingPage() {
|
|
15
|
+
return <PolicyPage policy={brand.shipping} />;
|
|
16
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
import type { Metadata } from "next";
|
|
2
|
+
import Link from "next/link";
|
|
3
|
+
import { cacheTag, cacheLife } from "next/cache";
|
|
4
|
+
import { getServerClient, tags, type Product } from "@cimplify/sdk/server";
|
|
5
|
+
import { brand } from "@/lib/brand";
|
|
6
|
+
|
|
7
|
+
export const metadata: Metadata = {
|
|
8
|
+
title: `Sitemap — ${brand.name}`,
|
|
9
|
+
description: "A human-readable index of every page on this site.",
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
interface SitemapData {
|
|
13
|
+
products: { slug: string; name: string }[];
|
|
14
|
+
categories: { slug: string; name: string }[];
|
|
15
|
+
collections: { slug: string; name: string }[];
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
async function getSitemap(): Promise<SitemapData> {
|
|
19
|
+
"use cache";
|
|
20
|
+
cacheTag(tags.products(), tags.categories(), tags.collections());
|
|
21
|
+
cacheLife("hours");
|
|
22
|
+
|
|
23
|
+
const client = getServerClient();
|
|
24
|
+
const [pRes, cRes, colRes] = await Promise.all([
|
|
25
|
+
client.catalogue.getProducts({ limit: 500 }),
|
|
26
|
+
client.catalogue.getCategories(),
|
|
27
|
+
client.catalogue.getCollections(),
|
|
28
|
+
]);
|
|
29
|
+
return {
|
|
30
|
+
products: (pRes.ok ? pRes.value.items : []).map((p: Product) => ({
|
|
31
|
+
slug: p.slug ?? p.id,
|
|
32
|
+
name: p.name,
|
|
33
|
+
})),
|
|
34
|
+
categories: (cRes.ok ? cRes.value : []).map((c) => ({ slug: c.slug, name: c.name })),
|
|
35
|
+
collections: (colRes.ok ? colRes.value : []).map((c) => ({ slug: c.slug, name: c.name })),
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const STATIC_LINKS: { title: string; links: { href: string; label: string }[] }[] = [
|
|
40
|
+
{
|
|
41
|
+
title: "Browse",
|
|
42
|
+
links: [
|
|
43
|
+
{ href: "/", label: "Home" },
|
|
44
|
+
{ href: "/shop", label: "Shop" },
|
|
45
|
+
{ href: "/search", label: "Search" },
|
|
46
|
+
],
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
title: "Account",
|
|
50
|
+
links: [
|
|
51
|
+
{ href: "/account", label: "Account" },
|
|
52
|
+
{ href: "/account/orders", label: "Orders" },
|
|
53
|
+
{ href: "/account/addresses", label: "Addresses" },
|
|
54
|
+
{ href: "/account/settings", label: "Settings" },
|
|
55
|
+
{ href: "/login", label: "Sign in" },
|
|
56
|
+
{ href: "/signup", label: "Create account" },
|
|
57
|
+
{ href: "/track-order", label: "Track an order" },
|
|
58
|
+
{ href: "/cart", label: "Cart" },
|
|
59
|
+
{ href: "/checkout", label: "Checkout" },
|
|
60
|
+
],
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
title: "About",
|
|
64
|
+
links: [
|
|
65
|
+
{ href: "/about", label: "About" },
|
|
66
|
+
{ href: "/faq", label: "FAQ" },
|
|
67
|
+
{ href: "/contact", label: "Contact" },
|
|
68
|
+
],
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
title: "Policies",
|
|
72
|
+
links: [
|
|
73
|
+
{ href: "/shipping", label: "Shipping" },
|
|
74
|
+
{ href: "/returns", label: "Returns" },
|
|
75
|
+
{ href: "/accessibility", label: "Accessibility" },
|
|
76
|
+
{ href: "/terms", label: "Terms of Service" },
|
|
77
|
+
{ href: "/privacy", label: "Privacy Policy" },
|
|
78
|
+
],
|
|
79
|
+
},
|
|
80
|
+
{
|
|
81
|
+
title: "Machine-readable",
|
|
82
|
+
links: [
|
|
83
|
+
{ href: "/sitemap.xml", label: "sitemap.xml (search engines)" },
|
|
84
|
+
{ href: "/llms.txt", label: "llms.txt (LLM agents)" },
|
|
85
|
+
{ href: "/robots.txt", label: "robots.txt" },
|
|
86
|
+
{ href: "/opensearch.xml", label: "opensearch.xml (browser search)" },
|
|
87
|
+
],
|
|
88
|
+
},
|
|
89
|
+
];
|
|
90
|
+
|
|
91
|
+
export default async function SitemapHtmlPage() {
|
|
92
|
+
const { products, categories, collections } = await getSitemap();
|
|
93
|
+
|
|
94
|
+
return (
|
|
95
|
+
<article className="max-w-5xl mx-auto px-6 sm:px-8 py-16">
|
|
96
|
+
<p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-primary mb-2">
|
|
97
|
+
Sitemap
|
|
98
|
+
</p>
|
|
99
|
+
<h1 className="text-[clamp(2rem,4vw,2.75rem)] font-semibold mb-2 -tracking-[0.02em]">
|
|
100
|
+
Every page, in one place.
|
|
101
|
+
</h1>
|
|
102
|
+
<p className="text-muted-foreground mb-12">
|
|
103
|
+
For search engines, see <Link href="/sitemap.xml" className="text-primary hover:underline">/sitemap.xml</Link>.
|
|
104
|
+
For LLM agents, see <Link href="/llms.txt" className="text-primary hover:underline">/llms.txt</Link>.
|
|
105
|
+
</p>
|
|
106
|
+
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-10">
|
|
107
|
+
{STATIC_LINKS.map((s) => (
|
|
108
|
+
<Section key={s.title} title={s.title}>
|
|
109
|
+
{s.links.map((l) => (
|
|
110
|
+
<li key={l.href}>
|
|
111
|
+
<Link href={l.href} className="hover:text-primary transition-colors">
|
|
112
|
+
{l.label}
|
|
113
|
+
</Link>
|
|
114
|
+
</li>
|
|
115
|
+
))}
|
|
116
|
+
</Section>
|
|
117
|
+
))}
|
|
118
|
+
{categories.length > 0 && (
|
|
119
|
+
<Section title="Categories">
|
|
120
|
+
{categories.map((c) => (
|
|
121
|
+
<li key={c.slug}>
|
|
122
|
+
<Link href={`/categories/${c.slug}`} className="hover:text-primary transition-colors">
|
|
123
|
+
{c.name}
|
|
124
|
+
</Link>
|
|
125
|
+
</li>
|
|
126
|
+
))}
|
|
127
|
+
</Section>
|
|
128
|
+
)}
|
|
129
|
+
{collections.length > 0 && (
|
|
130
|
+
<Section title="Collections">
|
|
131
|
+
{collections.map((c) => (
|
|
132
|
+
<li key={c.slug}>
|
|
133
|
+
<Link href={`/collections/${c.slug}`} className="hover:text-primary transition-colors">
|
|
134
|
+
{c.name}
|
|
135
|
+
</Link>
|
|
136
|
+
</li>
|
|
137
|
+
))}
|
|
138
|
+
</Section>
|
|
139
|
+
)}
|
|
140
|
+
{products.length > 0 && (
|
|
141
|
+
<Section title={`Products (${products.length})`}>
|
|
142
|
+
{products.map((p) => (
|
|
143
|
+
<li key={p.slug}>
|
|
144
|
+
<Link href={`/shop?product=${encodeURIComponent(p.slug)}`} className="hover:text-primary transition-colors">
|
|
145
|
+
{p.name}
|
|
146
|
+
</Link>
|
|
147
|
+
</li>
|
|
148
|
+
))}
|
|
149
|
+
</Section>
|
|
150
|
+
)}
|
|
151
|
+
</div>
|
|
152
|
+
</article>
|
|
153
|
+
);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function Section({ title, children }: { title: string; children: React.ReactNode }) {
|
|
157
|
+
return (
|
|
158
|
+
<div>
|
|
159
|
+
<p className="font-semibold text-[12px] uppercase tracking-[0.12em] text-foreground mb-3">
|
|
160
|
+
{title}
|
|
161
|
+
</p>
|
|
162
|
+
<ul className="space-y-2 m-0 p-0 list-none text-sm text-muted-foreground">
|
|
163
|
+
{children}
|
|
164
|
+
</ul>
|
|
165
|
+
</div>
|
|
166
|
+
);
|
|
167
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { Metadata } from "next";
|
|
2
|
+
import { TrackOrderForm } from "./track-order-form";
|
|
3
|
+
import { brand } from "@/lib/brand";
|
|
4
|
+
|
|
5
|
+
export const metadata: Metadata = {
|
|
6
|
+
title: `Track an order — ${brand.name}`,
|
|
7
|
+
description: brand.trackOrder.body,
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
export default function TrackOrderPage() {
|
|
11
|
+
const t = brand.trackOrder;
|
|
12
|
+
return (
|
|
13
|
+
<article className="max-w-2xl mx-auto px-6 sm:px-8 py-16">
|
|
14
|
+
<p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-primary mb-2">
|
|
15
|
+
{t.eyebrow}
|
|
16
|
+
</p>
|
|
17
|
+
<h1 className="font-serif text-[clamp(2rem,5vw,3rem)] font-semibold mb-4 -tracking-[0.02em]">
|
|
18
|
+
{t.title}
|
|
19
|
+
</h1>
|
|
20
|
+
<p className="text-muted-foreground leading-relaxed mb-8">{t.body}</p>
|
|
21
|
+
<TrackOrderForm />
|
|
22
|
+
</article>
|
|
23
|
+
);
|
|
24
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useState } from "react";
|
|
4
|
+
import { useRouter } from "next/navigation";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Guest order tracker. Routes /orders/<id> with the entered id; the
|
|
8
|
+
* post-checkout confirmation page already lives at /orders/[id] so this
|
|
9
|
+
* just sends the visitor there. Replace the redirect with a real lookup
|
|
10
|
+
* (e.g. Cimplify orders API + email-match guard) for production.
|
|
11
|
+
*/
|
|
12
|
+
export function TrackOrderForm() {
|
|
13
|
+
const router = useRouter();
|
|
14
|
+
const [orderId, setOrderId] = useState("");
|
|
15
|
+
const [email, setEmail] = useState("");
|
|
16
|
+
|
|
17
|
+
return (
|
|
18
|
+
<form
|
|
19
|
+
onSubmit={(e) => {
|
|
20
|
+
e.preventDefault();
|
|
21
|
+
if (!orderId.trim()) return;
|
|
22
|
+
router.push(`/orders/${encodeURIComponent(orderId.trim())}`);
|
|
23
|
+
}}
|
|
24
|
+
className="space-y-4 rounded-2xl border border-border bg-card p-6"
|
|
25
|
+
>
|
|
26
|
+
<div>
|
|
27
|
+
<label
|
|
28
|
+
htmlFor="orderId"
|
|
29
|
+
className="text-xs font-semibold uppercase tracking-wider text-muted-foreground block mb-1.5"
|
|
30
|
+
>
|
|
31
|
+
Order number
|
|
32
|
+
</label>
|
|
33
|
+
<input
|
|
34
|
+
id="orderId"
|
|
35
|
+
name="orderId"
|
|
36
|
+
required
|
|
37
|
+
value={orderId}
|
|
38
|
+
onChange={(e) => setOrderId(e.target.value)}
|
|
39
|
+
placeholder="ord_abc123…"
|
|
40
|
+
className="w-full px-4 py-3 rounded-md bg-background border border-border focus:border-primary focus:ring-2 focus:ring-primary/20 outline-none text-sm"
|
|
41
|
+
/>
|
|
42
|
+
</div>
|
|
43
|
+
<div>
|
|
44
|
+
<label
|
|
45
|
+
htmlFor="email"
|
|
46
|
+
className="text-xs font-semibold uppercase tracking-wider text-muted-foreground block mb-1.5"
|
|
47
|
+
>
|
|
48
|
+
Order email
|
|
49
|
+
</label>
|
|
50
|
+
<input
|
|
51
|
+
id="email"
|
|
52
|
+
name="email"
|
|
53
|
+
type="email"
|
|
54
|
+
required
|
|
55
|
+
value={email}
|
|
56
|
+
onChange={(e) => setEmail(e.target.value)}
|
|
57
|
+
placeholder="you@email.com"
|
|
58
|
+
className="w-full px-4 py-3 rounded-md bg-background border border-border focus:border-primary focus:ring-2 focus:ring-primary/20 outline-none text-sm"
|
|
59
|
+
/>
|
|
60
|
+
</div>
|
|
61
|
+
<button
|
|
62
|
+
type="submit"
|
|
63
|
+
className="w-full inline-flex items-center justify-center px-5 py-3 rounded-md bg-primary text-primary-foreground font-semibold text-sm hover:bg-primary/90 transition-colors"
|
|
64
|
+
>
|
|
65
|
+
Track order
|
|
66
|
+
</button>
|
|
67
|
+
</form>
|
|
68
|
+
);
|
|
69
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { CimplifyAccount } from "@cimplify/sdk/react";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Cimplify Account portal — iframe-mounted UI hosted by Cimplify Link.
|
|
7
|
+
* Handles sign-in, sign-up, OTP, addresses, payment methods, sessions,
|
|
8
|
+
* and order history. The iframe owns auth state; we just choose which
|
|
9
|
+
* `section` to land on.
|
|
10
|
+
*/
|
|
11
|
+
export function AccountIframe({ section }: { section?: string }) {
|
|
12
|
+
return <CimplifyAccount section={section} />;
|
|
13
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import type { BrandPolicySection } from "@/lib/brand";
|
|
2
|
+
|
|
3
|
+
interface PolicyShape {
|
|
4
|
+
eyebrow: string;
|
|
5
|
+
title: string;
|
|
6
|
+
lastUpdated?: string;
|
|
7
|
+
sections: BrandPolicySection[];
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Shared layout for shipping / returns / accessibility / terms / privacy.
|
|
12
|
+
* Reads a `{ eyebrow, title, lastUpdated, sections[] }` block from brand.
|
|
13
|
+
*/
|
|
14
|
+
export function PolicyPage({ policy }: { policy: PolicyShape }) {
|
|
15
|
+
return (
|
|
16
|
+
<article className="max-w-3xl mx-auto px-8 py-16 prose prose-lg max-w-none">
|
|
17
|
+
<p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-primary mb-2 not-prose">
|
|
18
|
+
{policy.eyebrow}
|
|
19
|
+
</p>
|
|
20
|
+
<h1 className="text-[clamp(2.25rem,5vw,3.5rem)] font-semibold mb-2 -tracking-[0.02em]">
|
|
21
|
+
{policy.title}
|
|
22
|
+
</h1>
|
|
23
|
+
{policy.lastUpdated && (
|
|
24
|
+
<p className="text-sm text-muted-foreground not-prose mb-10">
|
|
25
|
+
Last updated: {policy.lastUpdated}
|
|
26
|
+
</p>
|
|
27
|
+
)}
|
|
28
|
+
<section className="space-y-5 leading-relaxed text-foreground/90">
|
|
29
|
+
{policy.sections.map((s) => (
|
|
30
|
+
<div key={s.heading}>
|
|
31
|
+
<h2 className="text-2xl font-semibold mt-0">{s.heading}</h2>
|
|
32
|
+
{typeof s.body === "string" ? (
|
|
33
|
+
<p>{s.body}</p>
|
|
34
|
+
) : (
|
|
35
|
+
<>
|
|
36
|
+
<p>{s.body.intro}</p>
|
|
37
|
+
<ul className="list-disc pl-6 space-y-2">
|
|
38
|
+
{s.body.bullets.map((b) => (
|
|
39
|
+
<li key={b}>{b}</li>
|
|
40
|
+
))}
|
|
41
|
+
</ul>
|
|
42
|
+
</>
|
|
43
|
+
)}
|
|
44
|
+
</div>
|
|
45
|
+
))}
|
|
46
|
+
</section>
|
|
47
|
+
</article>
|
|
48
|
+
);
|
|
49
|
+
}
|
|
@@ -58,6 +58,20 @@ export interface Brand {
|
|
|
58
58
|
};
|
|
59
59
|
terms: { eyebrow: string; title: string; lastUpdated: string; sections: BrandPolicySection[] };
|
|
60
60
|
privacy: { eyebrow: string; title: string; lastUpdated: string; sections: BrandPolicySection[] };
|
|
61
|
+
shipping: { eyebrow: string; title: string; lastUpdated: string; sections: BrandPolicySection[] };
|
|
62
|
+
returns: { eyebrow: string; title: string; lastUpdated: string; sections: BrandPolicySection[] };
|
|
63
|
+
accessibility: { eyebrow: string; title: string; lastUpdated: string; sections: BrandPolicySection[] };
|
|
64
|
+
account: {
|
|
65
|
+
loginEyebrow: string; loginTitle: string; loginSubtitle: string;
|
|
66
|
+
signupEyebrow: string; signupTitle: string; signupSubtitle: string;
|
|
67
|
+
accountEyebrow: string; accountTitle: string;
|
|
68
|
+
};
|
|
69
|
+
contactPage: {
|
|
70
|
+
eyebrow: string; title: string; body: string;
|
|
71
|
+
reasons: string[];
|
|
72
|
+
directLines: { label: string; value: string; href: string }[];
|
|
73
|
+
};
|
|
74
|
+
trackOrder: { eyebrow: string; title: string; body: string };
|
|
61
75
|
footer: {
|
|
62
76
|
blurb: string; sitemap: BrandSitemapSection[];
|
|
63
77
|
poweredBy?: { label: string; href: string };
|
|
@@ -251,6 +265,70 @@ export const brand: Brand = {
|
|
|
251
265
|
],
|
|
252
266
|
},
|
|
253
267
|
|
|
268
|
+
shipping: {
|
|
269
|
+
eyebrow: "At the studio",
|
|
270
|
+
title: "What to expect",
|
|
271
|
+
lastUpdated: "1 May 2026",
|
|
272
|
+
sections: [
|
|
273
|
+
{ heading: "Arriving", body: "10 minutes early on your first visit (brief intake). 5 minutes for return visits. Tea on arrival." },
|
|
274
|
+
{ heading: "Parking", body: "Free street parking on Ebony Close. Free guest parking inside the compound for couples bookings." },
|
|
275
|
+
{ heading: "What we provide", body: "Robe, slippers, towels, locker. Bring nothing." },
|
|
276
|
+
{ heading: "Couples & guests", body: "Couples rooms accommodate two. For solo bookings, your guest is welcome to wait in the garden lounge." },
|
|
277
|
+
],
|
|
278
|
+
},
|
|
279
|
+
|
|
280
|
+
returns: {
|
|
281
|
+
eyebrow: "Cancellations",
|
|
282
|
+
title: "Cancellations & Refunds",
|
|
283
|
+
lastUpdated: "1 May 2026",
|
|
284
|
+
sections: [
|
|
285
|
+
{ heading: "Cancellation window", body: "Free cancellation up to 24 hours before your appointment. Within 24 hours, we charge 50% of the treatment cost." },
|
|
286
|
+
{ heading: "Late arrival", body: "Treatments end at the scheduled time. Late arrival shortens the treatment, not the cost." },
|
|
287
|
+
{ heading: "Membership pause", body: "Pause any month before the next billing date — no charge, no questions." },
|
|
288
|
+
{ heading: "Gift cards", body: "Non-refundable, no expiry. Transferable to anyone with the code." },
|
|
289
|
+
],
|
|
290
|
+
},
|
|
291
|
+
|
|
292
|
+
accessibility: {
|
|
293
|
+
eyebrow: "Accessibility",
|
|
294
|
+
title: "Accessibility Statement",
|
|
295
|
+
lastUpdated: "1 May 2026",
|
|
296
|
+
sections: [
|
|
297
|
+
{ heading: "At the studio", body: "Step-free entrance, accessible bathroom, and a treatment table that lowers to chair-transfer height. Service dogs welcome." },
|
|
298
|
+
{ heading: "Online", body: "We aim for WCAG 2.1 AA on this site." },
|
|
299
|
+
{ heading: "Reporting issues", body: "Email accessibility@serenespa.test or WhatsApp the studio. We respond within a business day." },
|
|
300
|
+
],
|
|
301
|
+
},
|
|
302
|
+
|
|
303
|
+
account: {
|
|
304
|
+
loginEyebrow: "Welcome back",
|
|
305
|
+
loginTitle: "Sign in to Serene",
|
|
306
|
+
loginSubtitle: "Manage your bookings, your membership, and your preferences.",
|
|
307
|
+
signupEyebrow: "Welcome",
|
|
308
|
+
signupTitle: "Create your account",
|
|
309
|
+
signupSubtitle: "Save your treatment history, your therapist preferences, and your favourite blends.",
|
|
310
|
+
accountEyebrow: "Your account",
|
|
311
|
+
accountTitle: "Welcome back",
|
|
312
|
+
},
|
|
313
|
+
|
|
314
|
+
contactPage: {
|
|
315
|
+
eyebrow: "Contact",
|
|
316
|
+
title: "How can we help?",
|
|
317
|
+
body: "Booking question, gift-card enquiry, or just want to say hello — we'll respond within a business day.",
|
|
318
|
+
reasons: ["Booking question", "Membership", "Gift cards", "Private events", "Press / partnerships", "Something else"],
|
|
319
|
+
directLines: [
|
|
320
|
+
{ label: "Email", value: "hello@serenespa.test", href: "mailto:hello@serenespa.test" },
|
|
321
|
+
{ label: "Phone", value: "+233 244 777 222", href: "tel:+233244777222" },
|
|
322
|
+
{ label: "WhatsApp", value: "+233 244 777 222", href: "https://wa.me/233244777222" },
|
|
323
|
+
],
|
|
324
|
+
},
|
|
325
|
+
|
|
326
|
+
trackOrder: {
|
|
327
|
+
eyebrow: "Find a booking",
|
|
328
|
+
title: "Look up your booking.",
|
|
329
|
+
body: "Enter your booking reference and the email used at booking. We'll show the time, the therapist, and any preparation notes.",
|
|
330
|
+
},
|
|
331
|
+
|
|
254
332
|
footer: {
|
|
255
333
|
blurb:
|
|
256
334
|
"A small spa in Cantonments. Massage, facials, hands & feet — by qualified, kind therapists.",
|
|
@@ -275,9 +353,20 @@ export const brand: Brand = {
|
|
|
275
353
|
{
|
|
276
354
|
title: "Help",
|
|
277
355
|
links: [
|
|
278
|
-
{ label: "Contact", href: "
|
|
356
|
+
{ label: "Contact", href: "/contact" },
|
|
357
|
+
{ label: "Find a booking", href: "/track-order" },
|
|
358
|
+
{ label: "What to expect", href: "/shipping" },
|
|
359
|
+
{ label: "Cancellations", href: "/returns" },
|
|
279
360
|
{ label: "FAQ", href: "/faq" },
|
|
280
|
-
|
|
361
|
+
],
|
|
362
|
+
},
|
|
363
|
+
{
|
|
364
|
+
title: "Account",
|
|
365
|
+
links: [
|
|
366
|
+
{ label: "Sign in", href: "/login" },
|
|
367
|
+
{ label: "Create account", href: "/signup" },
|
|
368
|
+
{ label: "Your bookings", href: "/account/orders" },
|
|
369
|
+
{ label: "Settings", href: "/account/settings" },
|
|
281
370
|
],
|
|
282
371
|
},
|
|
283
372
|
{
|
|
@@ -285,6 +374,8 @@ export const brand: Brand = {
|
|
|
285
374
|
links: [
|
|
286
375
|
{ label: "Terms of Service", href: "/terms" },
|
|
287
376
|
{ label: "Privacy Policy", href: "/privacy" },
|
|
377
|
+
{ label: "Accessibility", href: "/accessibility" },
|
|
378
|
+
{ label: "Sitemap", href: "/sitemap-page" },
|
|
288
379
|
],
|
|
289
380
|
},
|
|
290
381
|
],
|