@cimplify/sdk 0.41.0 → 0.42.1

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.
Files changed (148) hide show
  1. package/dist/cli/commands/deploy.mjs +561 -0
  2. package/dist/cli/commands/dev.mjs +434 -0
  3. package/dist/cli/commands/domains.mjs +479 -0
  4. package/dist/cli/commands/env.mjs +610 -0
  5. package/dist/cli/commands/link.mjs +325 -0
  6. package/dist/cli/commands/login.mjs +515 -0
  7. package/dist/cli/commands/logout.mjs +54 -0
  8. package/dist/cli/commands/logs.mjs +358 -0
  9. package/dist/cli/commands/projects.mjs +327 -0
  10. package/dist/cli/commands/rollback.mjs +477 -0
  11. package/dist/cli/commands/status.mjs +380 -0
  12. package/dist/cli/commands/unlink.mjs +58 -0
  13. package/dist/cli/commands/whoami.mjs +234 -0
  14. package/dist/dispatcher.mjs +91 -0
  15. package/dist/styles.css +1 -1
  16. package/package.json +1 -1
  17. package/templates/storefront-bakery/AGENTS.md +61 -10
  18. package/templates/storefront-bakery/app/accessibility/page.tsx +11 -0
  19. package/templates/storefront-bakery/app/account/addresses/page.tsx +21 -0
  20. package/templates/storefront-bakery/app/account/orders/page.tsx +21 -0
  21. package/templates/storefront-bakery/app/account/page.tsx +22 -0
  22. package/templates/storefront-bakery/app/account/settings/page.tsx +21 -0
  23. package/templates/storefront-bakery/app/contact/contact-form.tsx +109 -0
  24. package/templates/storefront-bakery/app/contact/page.tsx +54 -0
  25. package/templates/storefront-bakery/app/login/page.tsx +17 -0
  26. package/templates/storefront-bakery/app/opensearch.xml/route.ts +37 -0
  27. package/templates/storefront-bakery/app/returns/page.tsx +11 -0
  28. package/templates/storefront-bakery/app/search/page.tsx +38 -0
  29. package/templates/storefront-bakery/app/search/search-client.tsx +7 -0
  30. package/templates/storefront-bakery/app/shipping/page.tsx +16 -0
  31. package/templates/storefront-bakery/app/signup/page.tsx +17 -0
  32. package/templates/storefront-bakery/app/sitemap-page/page.tsx +167 -0
  33. package/templates/storefront-bakery/app/track-order/page.tsx +24 -0
  34. package/templates/storefront-bakery/app/track-order/track-order-form.tsx +69 -0
  35. package/templates/storefront-bakery/components/account-iframe.tsx +13 -0
  36. package/templates/storefront-bakery/components/policy-page.tsx +49 -0
  37. package/templates/storefront-bakery/lib/brand.ts +119 -4
  38. package/templates/storefront-fashion/AGENTS.md +80 -26
  39. package/templates/storefront-fashion/app/accessibility/page.tsx +11 -0
  40. package/templates/storefront-fashion/app/account/addresses/page.tsx +21 -0
  41. package/templates/storefront-fashion/app/account/orders/page.tsx +21 -0
  42. package/templates/storefront-fashion/app/account/page.tsx +22 -0
  43. package/templates/storefront-fashion/app/account/settings/page.tsx +21 -0
  44. package/templates/storefront-fashion/app/contact/contact-form.tsx +109 -0
  45. package/templates/storefront-fashion/app/contact/page.tsx +54 -0
  46. package/templates/storefront-fashion/app/login/page.tsx +17 -0
  47. package/templates/storefront-fashion/app/lookbook/page.tsx +132 -0
  48. package/templates/storefront-fashion/app/opensearch.xml/route.ts +37 -0
  49. package/templates/storefront-fashion/app/returns/page.tsx +11 -0
  50. package/templates/storefront-fashion/app/search/page.tsx +38 -0
  51. package/templates/storefront-fashion/app/search/search-client.tsx +7 -0
  52. package/templates/storefront-fashion/app/shipping/page.tsx +16 -0
  53. package/templates/storefront-fashion/app/signup/page.tsx +17 -0
  54. package/templates/storefront-fashion/app/sitemap-page/page.tsx +167 -0
  55. package/templates/storefront-fashion/app/size-guide/page.tsx +155 -0
  56. package/templates/storefront-fashion/app/track-order/page.tsx +24 -0
  57. package/templates/storefront-fashion/app/track-order/track-order-form.tsx +69 -0
  58. package/templates/storefront-fashion/components/account-iframe.tsx +13 -0
  59. package/templates/storefront-fashion/components/policy-page.tsx +49 -0
  60. package/templates/storefront-fashion/lib/brand.ts +99 -4
  61. package/templates/storefront-grocery/AGENTS.md +56 -11
  62. package/templates/storefront-grocery/app/accessibility/page.tsx +11 -0
  63. package/templates/storefront-grocery/app/account/addresses/page.tsx +21 -0
  64. package/templates/storefront-grocery/app/account/orders/page.tsx +21 -0
  65. package/templates/storefront-grocery/app/account/page.tsx +22 -0
  66. package/templates/storefront-grocery/app/account/settings/page.tsx +21 -0
  67. package/templates/storefront-grocery/app/contact/contact-form.tsx +109 -0
  68. package/templates/storefront-grocery/app/contact/page.tsx +54 -0
  69. package/templates/storefront-grocery/app/login/page.tsx +17 -0
  70. package/templates/storefront-grocery/app/opensearch.xml/route.ts +37 -0
  71. package/templates/storefront-grocery/app/returns/page.tsx +11 -0
  72. package/templates/storefront-grocery/app/search/page.tsx +38 -0
  73. package/templates/storefront-grocery/app/search/search-client.tsx +7 -0
  74. package/templates/storefront-grocery/app/shipping/page.tsx +16 -0
  75. package/templates/storefront-grocery/app/signup/page.tsx +17 -0
  76. package/templates/storefront-grocery/app/sitemap-page/page.tsx +167 -0
  77. package/templates/storefront-grocery/app/track-order/page.tsx +24 -0
  78. package/templates/storefront-grocery/app/track-order/track-order-form.tsx +69 -0
  79. package/templates/storefront-grocery/components/account-iframe.tsx +13 -0
  80. package/templates/storefront-grocery/components/policy-page.tsx +49 -0
  81. package/templates/storefront-grocery/lib/brand.ts +92 -3
  82. package/templates/storefront-restaurant/AGENTS.md +64 -13
  83. package/templates/storefront-restaurant/app/accessibility/page.tsx +11 -0
  84. package/templates/storefront-restaurant/app/account/addresses/page.tsx +21 -0
  85. package/templates/storefront-restaurant/app/account/orders/page.tsx +21 -0
  86. package/templates/storefront-restaurant/app/account/page.tsx +22 -0
  87. package/templates/storefront-restaurant/app/account/settings/page.tsx +21 -0
  88. package/templates/storefront-restaurant/app/contact/contact-form.tsx +109 -0
  89. package/templates/storefront-restaurant/app/contact/page.tsx +54 -0
  90. package/templates/storefront-restaurant/app/login/page.tsx +17 -0
  91. package/templates/storefront-restaurant/app/opensearch.xml/route.ts +37 -0
  92. package/templates/storefront-restaurant/app/reservations/page.tsx +66 -0
  93. package/templates/storefront-restaurant/app/reservations/reservations-client.tsx +234 -0
  94. package/templates/storefront-restaurant/app/returns/page.tsx +11 -0
  95. package/templates/storefront-restaurant/app/search/page.tsx +38 -0
  96. package/templates/storefront-restaurant/app/search/search-client.tsx +7 -0
  97. package/templates/storefront-restaurant/app/shipping/page.tsx +16 -0
  98. package/templates/storefront-restaurant/app/signup/page.tsx +17 -0
  99. package/templates/storefront-restaurant/app/sitemap-page/page.tsx +167 -0
  100. package/templates/storefront-restaurant/app/track-order/page.tsx +24 -0
  101. package/templates/storefront-restaurant/app/track-order/track-order-form.tsx +69 -0
  102. package/templates/storefront-restaurant/components/account-iframe.tsx +13 -0
  103. package/templates/storefront-restaurant/components/policy-page.tsx +49 -0
  104. package/templates/storefront-restaurant/lib/brand.ts +91 -3
  105. package/templates/storefront-retail/AGENTS.md +65 -24
  106. package/templates/storefront-retail/app/accessibility/page.tsx +11 -0
  107. package/templates/storefront-retail/app/account/addresses/page.tsx +21 -0
  108. package/templates/storefront-retail/app/account/orders/page.tsx +21 -0
  109. package/templates/storefront-retail/app/account/page.tsx +22 -0
  110. package/templates/storefront-retail/app/account/settings/page.tsx +21 -0
  111. package/templates/storefront-retail/app/contact/contact-form.tsx +109 -0
  112. package/templates/storefront-retail/app/contact/page.tsx +54 -0
  113. package/templates/storefront-retail/app/login/page.tsx +17 -0
  114. package/templates/storefront-retail/app/opensearch.xml/route.ts +37 -0
  115. package/templates/storefront-retail/app/returns/page.tsx +11 -0
  116. package/templates/storefront-retail/app/search/page.tsx +38 -0
  117. package/templates/storefront-retail/app/search/search-client.tsx +7 -0
  118. package/templates/storefront-retail/app/shipping/page.tsx +16 -0
  119. package/templates/storefront-retail/app/signup/page.tsx +17 -0
  120. package/templates/storefront-retail/app/sitemap-page/page.tsx +167 -0
  121. package/templates/storefront-retail/app/track-order/page.tsx +24 -0
  122. package/templates/storefront-retail/app/track-order/track-order-form.tsx +69 -0
  123. package/templates/storefront-retail/components/account-iframe.tsx +13 -0
  124. package/templates/storefront-retail/components/policy-page.tsx +49 -0
  125. package/templates/storefront-retail/lib/brand.ts +105 -9
  126. package/templates/storefront-services/AGENTS.md +64 -16
  127. package/templates/storefront-services/app/accessibility/page.tsx +11 -0
  128. package/templates/storefront-services/app/account/addresses/page.tsx +21 -0
  129. package/templates/storefront-services/app/account/orders/page.tsx +21 -0
  130. package/templates/storefront-services/app/account/page.tsx +22 -0
  131. package/templates/storefront-services/app/account/settings/page.tsx +21 -0
  132. package/templates/storefront-services/app/book/book-client.tsx +195 -0
  133. package/templates/storefront-services/app/book/page.tsx +65 -0
  134. package/templates/storefront-services/app/contact/contact-form.tsx +109 -0
  135. package/templates/storefront-services/app/contact/page.tsx +54 -0
  136. package/templates/storefront-services/app/login/page.tsx +17 -0
  137. package/templates/storefront-services/app/opensearch.xml/route.ts +37 -0
  138. package/templates/storefront-services/app/returns/page.tsx +11 -0
  139. package/templates/storefront-services/app/search/page.tsx +38 -0
  140. package/templates/storefront-services/app/search/search-client.tsx +7 -0
  141. package/templates/storefront-services/app/shipping/page.tsx +16 -0
  142. package/templates/storefront-services/app/signup/page.tsx +17 -0
  143. package/templates/storefront-services/app/sitemap-page/page.tsx +167 -0
  144. package/templates/storefront-services/app/track-order/page.tsx +24 -0
  145. package/templates/storefront-services/app/track-order/track-order-form.tsx +69 -0
  146. package/templates/storefront-services/components/account-iframe.tsx +13 -0
  147. package/templates/storefront-services/components/policy-page.tsx +49 -0
  148. package/templates/storefront-services/lib/brand.ts +93 -2
@@ -0,0 +1,21 @@
1
+ import type { Metadata } from "next";
2
+ import { AccountIframe } from "@/components/account-iframe";
3
+ import { brand } from "@/lib/brand";
4
+
5
+ export const metadata: Metadata = {
6
+ title: `Orders — ${brand.name}`,
7
+ };
8
+
9
+ export default function OrdersPage() {
10
+ return (
11
+ <article className="max-w-5xl mx-auto px-6 sm:px-8 py-12">
12
+ <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-primary mb-2">
13
+ Account
14
+ </p>
15
+ <h1 className="font-serif text-[clamp(2rem,4vw,2.75rem)] font-semibold mb-8 -tracking-[0.02em]">
16
+ Your orders
17
+ </h1>
18
+ <AccountIframe section="orders" />
19
+ </article>
20
+ );
21
+ }
@@ -0,0 +1,22 @@
1
+ import type { Metadata } from "next";
2
+ import { AccountIframe } from "@/components/account-iframe";
3
+ import { brand } from "@/lib/brand";
4
+
5
+ export const metadata: Metadata = {
6
+ title: `Account — ${brand.name}`,
7
+ description: brand.account.signupSubtitle,
8
+ };
9
+
10
+ export default function AccountPage() {
11
+ return (
12
+ <article className="max-w-5xl mx-auto px-6 sm:px-8 py-12">
13
+ <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-primary mb-2">
14
+ {brand.account.accountEyebrow}
15
+ </p>
16
+ <h1 className="font-serif text-[clamp(2rem,4vw,2.75rem)] font-semibold mb-8 -tracking-[0.02em]">
17
+ {brand.account.accountTitle}
18
+ </h1>
19
+ <AccountIframe />
20
+ </article>
21
+ );
22
+ }
@@ -0,0 +1,21 @@
1
+ import type { Metadata } from "next";
2
+ import { AccountIframe } from "@/components/account-iframe";
3
+ import { brand } from "@/lib/brand";
4
+
5
+ export const metadata: Metadata = {
6
+ title: `Settings — ${brand.name}`,
7
+ };
8
+
9
+ export default function SettingsPage() {
10
+ return (
11
+ <article className="max-w-5xl mx-auto px-6 sm:px-8 py-12">
12
+ <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-primary mb-2">
13
+ Account
14
+ </p>
15
+ <h1 className="font-serif text-[clamp(2rem,4vw,2.75rem)] font-semibold mb-8 -tracking-[0.02em]">
16
+ Settings
17
+ </h1>
18
+ <AccountIframe section="settings" />
19
+ </article>
20
+ );
21
+ }
@@ -0,0 +1,109 @@
1
+ "use client";
2
+
3
+ import { useState } from "react";
4
+
5
+ /**
6
+ * Local-only contact form. On submit, fakes a successful response so the
7
+ * template ships a usable interaction. Wire to a real handler (Server
8
+ * Action posting to your CRM, support inbox, or Cimplify Support API)
9
+ * before going to production.
10
+ */
11
+ export function ContactForm({ reasons }: { reasons: string[] }) {
12
+ const [submitted, setSubmitted] = useState(false);
13
+ const [reason, setReason] = useState(reasons[0] ?? "");
14
+
15
+ if (submitted) {
16
+ return (
17
+ <div className="rounded-2xl border border-border bg-card p-8">
18
+ <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-primary mb-2">
19
+ ✓ Sent
20
+ </p>
21
+ <h2 className="font-serif text-2xl font-semibold mb-2">Thanks — we'll be in touch.</h2>
22
+ <p className="text-muted-foreground">
23
+ We respond within one business day. Check your spam folder if you don't see a
24
+ reply by tomorrow.
25
+ </p>
26
+ </div>
27
+ );
28
+ }
29
+
30
+ return (
31
+ <form
32
+ onSubmit={(e) => {
33
+ e.preventDefault();
34
+ setSubmitted(true);
35
+ }}
36
+ className="space-y-5"
37
+ >
38
+ <div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
39
+ <Field label="Your name" name="name" required />
40
+ <Field label="Email" name="email" type="email" required />
41
+ </div>
42
+ <div>
43
+ <label className="text-xs font-semibold uppercase tracking-wider text-muted-foreground block mb-1.5">
44
+ What's it about?
45
+ </label>
46
+ <select
47
+ name="reason"
48
+ value={reason}
49
+ onChange={(e) => setReason(e.target.value)}
50
+ className="w-full px-4 py-2.5 rounded-md bg-background border border-border focus:border-primary focus:ring-2 focus:ring-primary/20 outline-none text-sm"
51
+ >
52
+ {reasons.map((r) => (
53
+ <option key={r} value={r}>
54
+ {r}
55
+ </option>
56
+ ))}
57
+ </select>
58
+ </div>
59
+ <Field label="Order number (optional)" name="order" />
60
+ <div>
61
+ <label className="text-xs font-semibold uppercase tracking-wider text-muted-foreground block mb-1.5">
62
+ Message
63
+ </label>
64
+ <textarea
65
+ name="message"
66
+ required
67
+ rows={6}
68
+ className="w-full px-4 py-2.5 rounded-md bg-background border border-border focus:border-primary focus:ring-2 focus:ring-primary/20 outline-none text-sm resize-y"
69
+ />
70
+ </div>
71
+ <button
72
+ type="submit"
73
+ className="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"
74
+ >
75
+ Send message
76
+ </button>
77
+ </form>
78
+ );
79
+ }
80
+
81
+ function Field({
82
+ label,
83
+ name,
84
+ type = "text",
85
+ required,
86
+ }: {
87
+ label: string;
88
+ name: string;
89
+ type?: string;
90
+ required?: boolean;
91
+ }) {
92
+ return (
93
+ <div>
94
+ <label
95
+ htmlFor={name}
96
+ className="text-xs font-semibold uppercase tracking-wider text-muted-foreground block mb-1.5"
97
+ >
98
+ {label}
99
+ </label>
100
+ <input
101
+ id={name}
102
+ name={name}
103
+ type={type}
104
+ required={required}
105
+ className="w-full px-4 py-2.5 rounded-md bg-background border border-border focus:border-primary focus:ring-2 focus:ring-primary/20 outline-none text-sm"
106
+ />
107
+ </div>
108
+ );
109
+ }
@@ -0,0 +1,54 @@
1
+ import type { Metadata } from "next";
2
+ import { ContactForm } from "./contact-form";
3
+ import { brand } from "@/lib/brand";
4
+
5
+ export const metadata: Metadata = {
6
+ title: `Contact — ${brand.name}`,
7
+ description: brand.contactPage.body,
8
+ };
9
+
10
+ export default function ContactPage() {
11
+ const c = brand.contactPage;
12
+ return (
13
+ <article className="max-w-5xl 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
+ {c.eyebrow}
16
+ </p>
17
+ <h1 className="font-serif text-[clamp(2.25rem,5vw,3.5rem)] font-semibold mb-4 -tracking-[0.02em]">
18
+ {c.title}
19
+ </h1>
20
+ <p className="text-lg text-muted-foreground max-w-2xl mb-12 leading-relaxed">{c.body}</p>
21
+
22
+ <div className="grid grid-cols-1 lg:grid-cols-[1.5fr_1fr] gap-10 items-start">
23
+ <ContactForm reasons={c.reasons} />
24
+ <aside className="space-y-5 lg:pl-10 lg:border-l border-border">
25
+ <div>
26
+ <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-muted-foreground mb-3">
27
+ Direct lines
28
+ </p>
29
+ <ul className="space-y-3 list-none p-0 m-0">
30
+ {c.directLines.map((line) => (
31
+ <li key={line.label}>
32
+ <p className="text-xs text-muted-foreground m-0">{line.label}</p>
33
+ <a
34
+ href={line.href}
35
+ className="text-foreground hover:text-primary transition-colors"
36
+ >
37
+ {line.value}
38
+ </a>
39
+ </li>
40
+ ))}
41
+ </ul>
42
+ </div>
43
+ <div>
44
+ <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-muted-foreground mb-3">
45
+ Visit
46
+ </p>
47
+ <p className="text-foreground m-0">{brand.contact.address}</p>
48
+ <p className="text-xs text-muted-foreground mt-1">{brand.contact.hours}</p>
49
+ </div>
50
+ </aside>
51
+ </div>
52
+ </article>
53
+ );
54
+ }
@@ -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, "&amp;")
33
+ .replace(/</g, "&lt;")
34
+ .replace(/>/g, "&gt;")
35
+ .replace(/"/g, "&quot;")
36
+ .replace(/'/g, "&apos;");
37
+ }
@@ -0,0 +1,66 @@
1
+ import type { Metadata } from "next";
2
+ import { Suspense } from "react";
3
+ import { cacheTag, cacheLife } from "next/cache";
4
+ import { getServerClient, tags, type Product } from "@cimplify/sdk/server";
5
+ import { ReservationsClient } from "./reservations-client";
6
+ import { brand } from "@/lib/brand";
7
+
8
+ export const metadata: Metadata = {
9
+ title: `Reserve a table — ${brand.name}`,
10
+ description: "Pick a date, time, and party size. We'll hold your table.",
11
+ };
12
+
13
+ async function getSeatingOptions(): Promise<Product[]> {
14
+ "use cache";
15
+ cacheTag(tags.products());
16
+ cacheLife("hours");
17
+
18
+ const client = getServerClient();
19
+ const r = await client.catalogue.getProducts({ limit: 50 });
20
+ if (!r.ok) return [];
21
+ // Reservation options live as service products in the catalogue (e.g.
22
+ // "Two-top", "Four-top", "Long table"). Filter to type=service.
23
+ return r.value.items.filter((p) => p.type === "service");
24
+ }
25
+
26
+ export default async function ReservationsPage() {
27
+ const options = await getSeatingOptions();
28
+ return (
29
+ <article className="max-w-5xl mx-auto px-6 sm:px-8 py-14">
30
+ <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-primary mb-2">
31
+ Reservations
32
+ </p>
33
+ <h1 className="font-serif text-[clamp(2rem,5vw,3rem)] font-semibold mb-3 -tracking-[0.02em]">
34
+ Hold a table.
35
+ </h1>
36
+ <p className="text-muted-foreground leading-relaxed mb-10 max-w-xl">
37
+ Bookings open 21 days in advance. SMS confirmation within a minute. Free
38
+ cancellation up to 4 hours before.
39
+ </p>
40
+
41
+ <Suspense fallback={<ReservationsSkeleton />}>
42
+ <ReservationsClient options={options} />
43
+ </Suspense>
44
+ </article>
45
+ );
46
+ }
47
+
48
+ function ReservationsSkeleton() {
49
+ return (
50
+ <div className="grid grid-cols-1 lg:grid-cols-[1fr_1.4fr] gap-8">
51
+ <div className="space-y-2">
52
+ {Array.from({ length: 4 }).map((_, i) => (
53
+ <div key={i} className="h-16 bg-muted rounded-2xl animate-pulse" />
54
+ ))}
55
+ </div>
56
+ <div className="rounded-2xl border border-border bg-card p-8">
57
+ <div className="h-5 w-40 bg-muted rounded mb-4 animate-pulse" />
58
+ <div className="grid grid-cols-3 gap-2">
59
+ {Array.from({ length: 9 }).map((_, i) => (
60
+ <div key={i} className="h-10 bg-muted rounded animate-pulse" />
61
+ ))}
62
+ </div>
63
+ </div>
64
+ </div>
65
+ );
66
+ }
@@ -0,0 +1,234 @@
1
+ "use client";
2
+
3
+ import { useMemo, useState } from "react";
4
+ import { useRouter } from "next/navigation";
5
+ import type { Product } from "@cimplify/sdk";
6
+ import { useCart } from "@cimplify/sdk/react";
7
+
8
+ const PARTY_SIZES = [2, 3, 4, 5, 6, 8, 10, 12];
9
+
10
+ /**
11
+ * Reservation flow. Reservations are modelled as service-type products in
12
+ * the catalogue (e.g. "Two-top", "Four-top", "Long table"). The user picks
13
+ * a seating option, date, time, and party size; we add to cart with the
14
+ * reservation details as item notes; checkout finalises the booking.
15
+ */
16
+ export function ReservationsClient({ options }: { options: Product[] }) {
17
+ const router = useRouter();
18
+ const { addToCart } = useCart();
19
+ const [selectedOption, setSelectedOption] = useState<Product | undefined>(options[0]);
20
+ const [selectedDate, setSelectedDate] = useState<Date>(() => new Date());
21
+ const [selectedSlotKey, setSelectedSlotKey] = useState<string | null>(null);
22
+ const [partySize, setPartySize] = useState<number>(2);
23
+ const [notes, setNotes] = useState("");
24
+ const [submitting, setSubmitting] = useState(false);
25
+
26
+ const dates = useMemo(() => {
27
+ const now = new Date();
28
+ return Array.from({ length: 14 }).map((_, i) => {
29
+ const d = new Date(now);
30
+ d.setDate(now.getDate() + i);
31
+ d.setHours(0, 0, 0, 0);
32
+ return d;
33
+ });
34
+ }, []);
35
+
36
+ // Restaurant slots: lunch 12–14:30, dinner 18–21:30, every 30 min.
37
+ const slots = useMemo(() => {
38
+ const out: { date: Date; label: string; service: "Lunch" | "Dinner" }[] = [];
39
+ for (let h = 12; h <= 14; h++) {
40
+ for (const m of [0, 30]) {
41
+ const d = new Date(selectedDate);
42
+ d.setHours(h, m, 0, 0);
43
+ out.push({ date: d, label: `${String(h).padStart(2, "0")}:${String(m).padStart(2, "0")}`, service: "Lunch" });
44
+ }
45
+ }
46
+ for (let h = 18; h <= 21; h++) {
47
+ for (const m of [0, 30]) {
48
+ const d = new Date(selectedDate);
49
+ d.setHours(h, m, 0, 0);
50
+ out.push({ date: d, label: `${String(h).padStart(2, "0")}:${String(m).padStart(2, "0")}`, service: "Dinner" });
51
+ }
52
+ }
53
+ return out;
54
+ }, [selectedDate]);
55
+
56
+ async function confirm() {
57
+ if (!selectedOption || !selectedSlotKey) return;
58
+ setSubmitting(true);
59
+ try {
60
+ const when = new Date(selectedSlotKey).toLocaleString();
61
+ const note = `Party of ${partySize} · ${when}${notes ? ` · ${notes}` : ""}`;
62
+ await addToCart(selectedOption.id, 1, { notes: note });
63
+ router.push("/checkout");
64
+ } catch {
65
+ setSubmitting(false);
66
+ }
67
+ }
68
+
69
+ if (options.length === 0) {
70
+ return (
71
+ <p className="text-muted-foreground">
72
+ No bookable seating options yet. Add Service-type products to your catalogue
73
+ (e.g. "Two-top", "Four-top", "Long table for 12") to enable reservations.
74
+ </p>
75
+ );
76
+ }
77
+
78
+ return (
79
+ <div className="grid grid-cols-1 lg:grid-cols-[1fr_1.4fr] gap-8">
80
+ <div className="space-y-6">
81
+ <div>
82
+ <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-muted-foreground mb-3">
83
+ Seating
84
+ </p>
85
+ <div className="space-y-2">
86
+ {options.map((o) => {
87
+ const active = selectedOption?.id === o.id;
88
+ return (
89
+ <button
90
+ key={o.id}
91
+ type="button"
92
+ onClick={() => setSelectedOption(o)}
93
+ className={[
94
+ "w-full text-left rounded-2xl border p-4 transition-colors",
95
+ active
96
+ ? "border-primary bg-primary/5"
97
+ : "border-border bg-card hover:border-foreground/30",
98
+ ].join(" ")}
99
+ >
100
+ <p className="font-semibold text-sm m-0">{o.name}</p>
101
+ {o.description && (
102
+ <p className="text-xs text-muted-foreground mt-0.5 line-clamp-2">{o.description}</p>
103
+ )}
104
+ </button>
105
+ );
106
+ })}
107
+ </div>
108
+ </div>
109
+
110
+ <div>
111
+ <label
112
+ htmlFor="party"
113
+ className="text-[11px] font-semibold uppercase tracking-[0.16em] text-muted-foreground block mb-3"
114
+ >
115
+ Party size
116
+ </label>
117
+ <div className="grid grid-cols-4 gap-1.5">
118
+ {PARTY_SIZES.map((p) => {
119
+ const active = partySize === p;
120
+ return (
121
+ <button
122
+ key={p}
123
+ type="button"
124
+ onClick={() => setPartySize(p)}
125
+ className={[
126
+ "py-2 rounded-md text-sm font-semibold transition-colors",
127
+ active
128
+ ? "bg-foreground text-background"
129
+ : "bg-background border border-border hover:border-foreground",
130
+ ].join(" ")}
131
+ >
132
+ {p}
133
+ </button>
134
+ );
135
+ })}
136
+ </div>
137
+ </div>
138
+
139
+ <div>
140
+ <label
141
+ htmlFor="notes"
142
+ className="text-[11px] font-semibold uppercase tracking-[0.16em] text-muted-foreground block mb-3"
143
+ >
144
+ Notes (optional)
145
+ </label>
146
+ <textarea
147
+ id="notes"
148
+ rows={3}
149
+ value={notes}
150
+ onChange={(e) => setNotes(e.target.value)}
151
+ placeholder="Allergies, occasion, accessibility…"
152
+ className="w-full px-4 py-2.5 rounded-md bg-background border border-border focus:border-primary focus:ring-2 focus:ring-primary/20 outline-none text-sm resize-y"
153
+ />
154
+ </div>
155
+ </div>
156
+
157
+ <div className="rounded-2xl border border-border bg-card p-6">
158
+ <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-muted-foreground mb-3">
159
+ Date
160
+ </p>
161
+ <div className="grid grid-cols-7 gap-1.5 mb-6">
162
+ {dates.map((d) => {
163
+ const active = d.toDateString() === selectedDate.toDateString();
164
+ return (
165
+ <button
166
+ key={d.toISOString()}
167
+ type="button"
168
+ onClick={() => {
169
+ setSelectedDate(d);
170
+ setSelectedSlotKey(null);
171
+ }}
172
+ className={[
173
+ "flex flex-col items-center justify-center py-2 rounded-md transition-colors",
174
+ active
175
+ ? "bg-foreground text-background"
176
+ : "bg-background hover:bg-muted text-foreground",
177
+ ].join(" ")}
178
+ >
179
+ <span className="text-[10px] uppercase tracking-wider opacity-60">
180
+ {d.toLocaleString(undefined, { weekday: "short" })}
181
+ </span>
182
+ <span className="text-base font-semibold tabular-nums">{d.getDate()}</span>
183
+ </button>
184
+ );
185
+ })}
186
+ </div>
187
+
188
+ {(["Lunch", "Dinner"] as const).map((service) => (
189
+ <div key={service} className="mb-5 last:mb-0">
190
+ <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-muted-foreground mb-2">
191
+ {service}
192
+ </p>
193
+ <div className="grid grid-cols-3 sm:grid-cols-5 gap-2">
194
+ {slots
195
+ .filter((s) => s.service === service)
196
+ .map((s) => {
197
+ const key = s.date.toISOString();
198
+ const active = selectedSlotKey === key;
199
+ return (
200
+ <button
201
+ key={key}
202
+ type="button"
203
+ onClick={() => setSelectedSlotKey(key)}
204
+ className={[
205
+ "py-2 rounded-md text-sm tabular-nums transition-colors",
206
+ active
207
+ ? "bg-primary text-primary-foreground"
208
+ : "bg-background border border-border hover:border-primary",
209
+ ].join(" ")}
210
+ >
211
+ {s.label}
212
+ </button>
213
+ );
214
+ })}
215
+ </div>
216
+ </div>
217
+ ))}
218
+
219
+ <button
220
+ type="button"
221
+ onClick={confirm}
222
+ disabled={!selectedOption || !selectedSlotKey || submitting}
223
+ className="w-full mt-4 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"
224
+ >
225
+ {submitting
226
+ ? "Confirming…"
227
+ : selectedSlotKey
228
+ ? `Reserve for ${partySize} on ${new Date(selectedSlotKey).toLocaleDateString()} at ${new Date(selectedSlotKey).toLocaleTimeString(undefined, { hour: "2-digit", minute: "2-digit" })}`
229
+ : "Pick a slot to reserve"}
230
+ </button>
231
+ </div>
232
+ </div>
233
+ );
234
+ }
@@ -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,7 @@
1
+ "use client";
2
+
3
+ import { SearchPage } from "@cimplify/sdk/react";
4
+
5
+ export function SearchClient() {
6
+ return <SearchPage />;
7
+ }
@@ -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
+ }