@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,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
+ }
@@ -173,6 +173,28 @@ export interface Brand {
173
173
  sections: BrandPolicySection[];
174
174
  };
175
175
 
176
+ // ─── Standalone policy pages ──────────────────────────────────
177
+ shipping: { eyebrow: string; title: string; lastUpdated: string; sections: BrandPolicySection[] };
178
+ returns: { eyebrow: string; title: string; lastUpdated: string; sections: BrandPolicySection[] };
179
+ accessibility: { eyebrow: string; title: string; lastUpdated: string; sections: BrandPolicySection[] };
180
+
181
+ // ─── Auth + account UX copy ───────────────────────────────────
182
+ account: {
183
+ loginEyebrow: string; loginTitle: string; loginSubtitle: string;
184
+ signupEyebrow: string; signupTitle: string; signupSubtitle: string;
185
+ accountEyebrow: string; accountTitle: string;
186
+ };
187
+
188
+ // ─── Contact form copy ────────────────────────────────────────
189
+ contactPage: {
190
+ eyebrow: string; title: string; body: string;
191
+ reasons: string[];
192
+ directLines: { label: string; value: string; href: string }[];
193
+ };
194
+
195
+ // ─── Track-order page copy ────────────────────────────────────
196
+ trackOrder: { eyebrow: string; title: string; body: string };
197
+
176
198
  // ─── Footer ───────────────────────────────────────────────────
177
199
  footer: {
178
200
  blurb: string;
@@ -511,6 +533,77 @@ export const brand: Brand = {
511
533
  ],
512
534
  },
513
535
 
536
+ shipping: {
537
+ eyebrow: "Shipping",
538
+ title: "Shipping & Pickup",
539
+ lastUpdated: "1 May 2026",
540
+ sections: [
541
+ { heading: "Same-day Greater Accra", body: "Orders before 4pm ship the same day. Free over GH₵500." },
542
+ { heading: "Outside Accra", body: "Partner courier nationwide; 1–3 business days. Tracking SMS sent on dispatch." },
543
+ { heading: "International", body: "DHL Express across West Africa. Rates and ETA computed at checkout." },
544
+ { heading: "Pickup", body: "Atomic Junction store, Mon–Sat 9am–8pm. Choose Pickup at checkout; ready in 30 minutes." },
545
+ { heading: "Bulky / fragile items", body: "TVs, monitors, and large appliances ship by signature. We coordinate the delivery window with you 24h before." },
546
+ ],
547
+ },
548
+
549
+ returns: {
550
+ eyebrow: "Returns",
551
+ title: "Returns & Warranty",
552
+ lastUpdated: "1 May 2026",
553
+ sections: [
554
+ { heading: "Sealed products", body: "14 days, no questions asked. Original packaging required." },
555
+ { heading: "Opened products", body: "7 days, subject to a 10% restocking fee for non-defective items." },
556
+ { heading: "Activated SIM phones", body: "Cannot be returned once the SIM is registered. Defects covered under warranty." },
557
+ { heading: "Two-year warranty", body: "Manufacturer + Currents-backed extension. Covers manufacturing defects and battery degradation below 80%." },
558
+ { heading: "How to start", body: "Email support@currentselectronics.test with your order number. We email a prepaid label or pickup time." },
559
+ ],
560
+ },
561
+
562
+ accessibility: {
563
+ eyebrow: "Accessibility",
564
+ title: "Accessibility Statement",
565
+ lastUpdated: "1 May 2026",
566
+ sections: [
567
+ { heading: "Our commitment", body: "We aim for WCAG 2.1 AA on this site and test against it on every release." },
568
+ { heading: "What we've done", body: { intro: "Specifically, we've:", bullets: [
569
+ "Maintained a minimum 4.5:1 contrast ratio on body text.",
570
+ "Ensured every interactive element is keyboard-reachable with visible focus.",
571
+ "Labelled images, icons, and form fields for screen-reader users.",
572
+ "Avoided motion-based interactions; respected `prefers-reduced-motion`.",
573
+ ] } },
574
+ { heading: "Reporting issues", body: "Email accessibility@currentselectronics.test. We respond within 5 business days." },
575
+ ],
576
+ },
577
+
578
+ account: {
579
+ loginEyebrow: "Welcome back",
580
+ loginTitle: "Sign in to Currents",
581
+ loginSubtitle: "Track orders, manage your warranty, and view your trade-in credits.",
582
+ signupEyebrow: "Welcome",
583
+ signupTitle: "Create your Currents account",
584
+ signupSubtitle: "Save addresses, register warranties, manage trade-ins, and unlock instalment options at checkout.",
585
+ accountEyebrow: "Your account",
586
+ accountTitle: "Welcome back",
587
+ },
588
+
589
+ contactPage: {
590
+ eyebrow: "Contact",
591
+ title: "Talk to a real human.",
592
+ body: "Order question, warranty claim, business sales — fill the form and we'll reply within a business day.",
593
+ reasons: ["General question", "An order issue", "Warranty / repair", "Trade-in", "Business sales", "Press / partnerships"],
594
+ directLines: [
595
+ { label: "Email", value: "hello@currentselectronics.test", href: "mailto:hello@currentselectronics.test" },
596
+ { label: "Phone", value: "+233 244 000 000", href: "tel:+233244000000" },
597
+ { label: "WhatsApp", value: "+233 244 000 000", href: "https://wa.me/233244000000" },
598
+ ],
599
+ },
600
+
601
+ trackOrder: {
602
+ eyebrow: "Track an order",
603
+ title: "Where's my order?",
604
+ body: "Enter your order number and email. We'll show the live status, courier tracking, and ETA.",
605
+ },
606
+
514
607
  footer: {
515
608
  blurb:
516
609
  "Authorised dealer for Apple, Samsung, Sony, Bose. Same-day delivery in Accra; nationwide via partner courier. Two-year warranty on every product.",
@@ -529,19 +622,20 @@ export const brand: Brand = {
529
622
  {
530
623
  title: "Support",
531
624
  links: [
532
- { label: "Contact", href: "mailto:hello@currentselectronics.test" },
533
- { label: "Warranty", href: "/faq" },
534
- { label: "Returns", href: "/faq" },
535
- { label: "Shipping", href: "/faq" },
625
+ { label: "Contact", href: "/contact" },
626
+ { label: "Track an order", href: "/track-order" },
627
+ { label: "Shipping", href: "/shipping" },
628
+ { label: "Returns & warranty", href: "/returns" },
629
+ { label: "FAQ", href: "/faq" },
536
630
  ],
537
631
  },
538
632
  {
539
- title: "Company",
633
+ title: "Account",
540
634
  links: [
541
- { label: "About", href: "/about" },
542
- { label: "Trade-in", href: "/about" },
543
- { label: "Business sales", href: "mailto:business@currentselectronics.test" },
544
- { label: "Careers", href: "/about" },
635
+ { label: "Sign in", href: "/login" },
636
+ { label: "Create account", href: "/signup" },
637
+ { label: "Your orders", href: "/account/orders" },
638
+ { label: "Settings", href: "/account/settings" },
545
639
  ],
546
640
  },
547
641
  {
@@ -549,6 +643,8 @@ export const brand: Brand = {
549
643
  links: [
550
644
  { label: "Terms of Service", href: "/terms" },
551
645
  { label: "Privacy Policy", href: "/privacy" },
646
+ { label: "Accessibility", href: "/accessibility" },
647
+ { label: "Sitemap", href: "/sitemap-page" },
552
648
  ],
553
649
  },
554
650
  ],
@@ -5,31 +5,79 @@ If you are an AI agent (Claude, Cursor, Aider, devin, …) working on this store
5
5
  ## TL;DR for rebranding
6
6
 
7
7
  1. **Edit `lib/brand.ts`** — every visible string lives here.
8
- 2. **Edit `app/globals.css`** — the `@theme { … }` block holds the palette + radius.
8
+ 2. **Edit `app/globals.css`** — `@theme { … }` for palette + radius.
9
9
  3. **Edit `.env.local`** — `NEXT_PUBLIC_CIMPLIFY_BUSINESS_ID=bus_serene_spa` for the bundled seed.
10
10
 
11
11
  ## Aesthetic
12
12
 
13
- Calm, refined wellness — Booksy / Mindbody / boutique-spa territory:
13
+ - **Fraunces serif + Inter** — warm, slightly editorial headings.
14
+ - **Sand + sage palette** — warm cream background, deep sage primary.
15
+ - **Generous radius**: `1rem` — calm, soft.
16
+ - Lower-contrast than retail / fashion.
17
+ - Schema.org `@type` is `BeautySalon`.
14
18
 
15
- - **Fraunces serif** for headings (warm, slightly editorial).
16
- - **Inter** for body and UI.
17
- - **Sand + sage palette**: warm cream background, deep sage primary.
18
- - **Soft corners**: `--radius: 1rem` — generous rounding throughout.
19
- - **Lower-contrast** than retail / fashion; calm-feeling.
19
+ ## Page surface
20
20
 
21
- ## Industry-specific copy patterns
21
+ ```
22
+ app/
23
+ page.tsx Home — hero ("Now booking"), service strips,
24
+ category grid, newsletter
25
+ shop/page.tsx Treatment menu (SDK <CataloguePage/>)
26
+ search/page.tsx Search
27
+ collections/[slug]/page.tsx Collection landing
28
+ categories/[slug]/page.tsx Category landing (e.g. memberships, packages)
29
+
30
+ book/page.tsx ⭐ Services-specific: full booking widget
31
+ (treatment + 14-day date + 30-min slot grid)
32
+ → adds to cart with slot as note → /checkout
33
+
34
+ cart/page.tsx, checkout/page.tsx, orders/[id]/page.tsx
35
+
36
+ account/page.tsx <CimplifyAccount /> (iframe)
37
+ account/orders/page.tsx <CimplifyAccount section="orders" />
38
+ account/addresses/page.tsx <CimplifyAccount section="addresses" />
39
+ account/settings/page.tsx <CimplifyAccount section="settings" />
40
+ login/page.tsx, signup/page.tsx redirects → /account
41
+
42
+ contact/page.tsx, track-order/page.tsx (find a booking)
43
+
44
+ about/page.tsx, faq/page.tsx
45
+ shipping/page.tsx (what to expect at the studio), returns/page.tsx (cancellations),
46
+ accessibility/page.tsx, terms/page.tsx, privacy/page.tsx
47
+
48
+ sitemap-page/page.tsx, sitemap.ts, robots.ts, llms.txt/route.ts, opensearch.xml/route.ts
49
+ error.tsx, not-found.tsx
50
+ ```
51
+
52
+ ## File ↔ brand-field map
53
+
54
+ | File | Reads from `brand` |
55
+ |---|---|
56
+ | `app/layout.tsx` | identity, contact, socials, BeautySalon JSON-LD |
57
+ | `app/page.tsx` | `brand.hero` ("Now booking") |
58
+ | `app/about/page.tsx` | `brand.about` |
59
+ | `app/faq/page.tsx` | `brand.faq` (booking, contraindications, memberships) |
60
+ | `app/shipping/page.tsx` | `brand.shipping` (what to expect at the studio) |
61
+ | `app/returns/page.tsx` | `brand.returns` (cancellation policy) |
62
+ | `app/accessibility/page.tsx` | `brand.accessibility` |
63
+ | `app/terms/page.tsx`, `app/privacy/page.tsx` | `brand.terms`, `brand.privacy` |
64
+ | `app/contact/page.tsx` | `brand.contactPage`, `brand.contact` |
65
+ | `app/track-order/page.tsx` | `brand.trackOrder` ("find a booking") |
66
+ | `app/account/*/page.tsx` | `brand.account` (iframe owns UI) |
67
+ | `app/book/*` | derives bookable services from product list (`type: "service"`) |
68
+ | `app/llms.txt/route.ts` | `brand.llms`, contact, currency |
69
+ | `components/header.tsx`, `footer.tsx` | `brand.header`, `brand.footer`, `brand.contact`, `brand.socials` |
70
+
71
+ ## Services-specific notes
22
72
 
23
- The bundled brand presets:
24
- - `brand.hero.badge` reads `"Now booking · April"` — month-based availability.
25
- - `brand.hero.primaryCtaLabel` is `"Book a treatment"`.
26
- - `brand.tradeIn` is omitted (irrelevant for services).
27
- - `brand.faq` covers booking, cancellation, contraindications, memberships.
28
- - Schema.org type is `BeautySalon`.
73
+ - **Bookable services are `type: "service"` products.** The `/book` page filters the product list to type=service and treats them as treatments. Their `duration_minutes` is shown on the chip.
74
+ - Cart-item notes carry the booked time slot — checkout sends them through Cimplify's order pipeline. The merchant's downstream system reads the note to schedule the appointment.
75
+ - Mock seed: `--seed services` (Serene Spa).
29
76
 
30
- ## Mock seed
77
+ ## Known TODOs
31
78
 
32
- Wired to `--seed services` (Serene Spa). Products in this seed are *services* (`type: "service"`), so the SDK's variant cards render `StandardServiceCard` automatically.
79
+ - `/book` widget shows every slot as available. For production, wire `useAvailableSlots({ serviceId, date })` from `@cimplify/sdk/react` to filter to genuinely-free windows.
80
+ - Contact + newsletter fake submits.
33
81
 
34
82
  ## Quick start
35
83
 
@@ -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.accessibility.title} — ${brand.name}`,
7
+ };
8
+
9
+ export default function AccessibilityPage() {
10
+ return <PolicyPage policy={brand.accessibility} />;
11
+ }
@@ -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: `Addresses — ${brand.name}`,
7
+ };
8
+
9
+ export default function AddressesPage() {
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 addresses
17
+ </h1>
18
+ <AccountIframe section="addresses" />
19
+ </article>
20
+ );
21
+ }
@@ -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,195 @@
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
+ interface Slot {
9
+ date: Date;
10
+ label: string;
11
+ }
12
+
13
+ /**
14
+ * Real booking flow:
15
+ * 1. Pick a treatment (left rail)
16
+ * 2. Pick a date (chips, today + next 13 days)
17
+ * 3. Pick a slot (15-minute grid, 10am–7pm)
18
+ * 4. Add to cart with the chosen slot as a cart-item note;
19
+ * Cimplify Checkout finalises the booking.
20
+ *
21
+ * The slot grid is generated client-side as a placeholder. In production,
22
+ * call `useAvailableSlots({ serviceId, date })` from `@cimplify/sdk/react`
23
+ * to fetch real availability from the Cimplify scheduling API.
24
+ */
25
+ export function BookClient({ treatments }: { treatments: Product[] }) {
26
+ const router = useRouter();
27
+ const { addToCart } = useCart();
28
+ const [selectedTreatment, setSelectedTreatment] = useState<Product | undefined>(
29
+ treatments[0],
30
+ );
31
+ const [selectedDate, setSelectedDate] = useState<Date>(() => new Date());
32
+ const [selectedSlotKey, setSelectedSlotKey] = useState<string | null>(null);
33
+ const [submitting, setSubmitting] = useState(false);
34
+
35
+ const dates = useMemo(() => {
36
+ const now = new Date();
37
+ return Array.from({ length: 14 }).map((_, i) => {
38
+ const d = new Date(now);
39
+ d.setDate(now.getDate() + i);
40
+ d.setHours(0, 0, 0, 0);
41
+ return d;
42
+ });
43
+ }, []);
44
+
45
+ const slots = useMemo<Slot[]>(() => {
46
+ const out: Slot[] = [];
47
+ for (let h = 10; h <= 19; h++) {
48
+ for (const m of [0, 30]) {
49
+ const d = new Date(selectedDate);
50
+ d.setHours(h, m, 0, 0);
51
+ const label = `${String(h).padStart(2, "0")}:${String(m).padStart(2, "0")}`;
52
+ out.push({ date: d, label });
53
+ }
54
+ }
55
+ return out;
56
+ }, [selectedDate]);
57
+
58
+ const slotKey = (s: Slot) => s.date.toISOString();
59
+
60
+ async function confirm() {
61
+ if (!selectedTreatment || !selectedSlotKey) return;
62
+ setSubmitting(true);
63
+ try {
64
+ await addToCart(selectedTreatment.id, 1, {
65
+ notes: `Booked for ${new Date(selectedSlotKey).toLocaleString()}`,
66
+ });
67
+ router.push("/checkout");
68
+ } catch {
69
+ setSubmitting(false);
70
+ }
71
+ }
72
+
73
+ if (treatments.length === 0) {
74
+ return (
75
+ <p className="text-muted-foreground">
76
+ No bookable treatments yet. Add a Service-type product to your catalogue first.
77
+ </p>
78
+ );
79
+ }
80
+
81
+ return (
82
+ <div className="grid grid-cols-1 lg:grid-cols-[1fr_1.4fr] gap-8">
83
+ {/* Treatments */}
84
+ <div>
85
+ <p className="text-[11px] font-mono uppercase tracking-[0.16em] text-muted-foreground mb-3">
86
+ Treatment
87
+ </p>
88
+ <div className="space-y-2">
89
+ {treatments.map((t) => {
90
+ const active = selectedTreatment?.id === t.id;
91
+ return (
92
+ <button
93
+ key={t.id}
94
+ type="button"
95
+ onClick={() => setSelectedTreatment(t)}
96
+ className={[
97
+ "w-full text-left rounded-2xl border p-4 transition-colors",
98
+ active
99
+ ? "border-primary bg-primary/5"
100
+ : "border-border bg-card hover:border-foreground/30",
101
+ ].join(" ")}
102
+ >
103
+ <div className="flex items-center justify-between gap-3">
104
+ <div className="min-w-0">
105
+ <p className="font-semibold text-sm m-0 truncate">{t.name}</p>
106
+ <p className="text-xs text-muted-foreground m-0">
107
+ {t.duration_minutes ? `${t.duration_minutes} min · ` : ""}
108
+ {t.currency ?? "GHS"} {t.default_price}
109
+ </p>
110
+ </div>
111
+ {active && (
112
+ <span className="grid place-items-center w-6 h-6 rounded-full bg-primary text-primary-foreground text-xs">
113
+ ✓
114
+ </span>
115
+ )}
116
+ </div>
117
+ </button>
118
+ );
119
+ })}
120
+ </div>
121
+ </div>
122
+
123
+ {/* Date + slots */}
124
+ <div className="rounded-2xl border border-border bg-card p-6">
125
+ <p className="text-[11px] font-mono uppercase tracking-[0.16em] text-muted-foreground mb-3">
126
+ Date
127
+ </p>
128
+ <div className="grid grid-cols-7 gap-1.5 mb-6">
129
+ {dates.map((d) => {
130
+ const active = d.toDateString() === selectedDate.toDateString();
131
+ return (
132
+ <button
133
+ key={d.toISOString()}
134
+ type="button"
135
+ onClick={() => {
136
+ setSelectedDate(d);
137
+ setSelectedSlotKey(null);
138
+ }}
139
+ className={[
140
+ "flex flex-col items-center justify-center py-2 rounded-md transition-colors",
141
+ active
142
+ ? "bg-foreground text-background"
143
+ : "bg-background hover:bg-muted text-foreground",
144
+ ].join(" ")}
145
+ >
146
+ <span className="text-[10px] uppercase tracking-wider opacity-60">
147
+ {d.toLocaleString(undefined, { weekday: "short" })}
148
+ </span>
149
+ <span className="text-base font-semibold tabular-nums">{d.getDate()}</span>
150
+ </button>
151
+ );
152
+ })}
153
+ </div>
154
+
155
+ <p className="text-[11px] font-mono uppercase tracking-[0.16em] text-muted-foreground mb-3">
156
+ Slot
157
+ </p>
158
+ <div className="grid grid-cols-3 sm:grid-cols-4 gap-2">
159
+ {slots.map((s) => {
160
+ const key = slotKey(s);
161
+ const active = selectedSlotKey === key;
162
+ return (
163
+ <button
164
+ key={key}
165
+ type="button"
166
+ onClick={() => setSelectedSlotKey(key)}
167
+ className={[
168
+ "py-2 rounded-md text-sm tabular-nums transition-colors",
169
+ active
170
+ ? "bg-primary text-primary-foreground"
171
+ : "bg-background border border-border hover:border-primary",
172
+ ].join(" ")}
173
+ >
174
+ {s.label}
175
+ </button>
176
+ );
177
+ })}
178
+ </div>
179
+
180
+ <button
181
+ type="button"
182
+ onClick={confirm}
183
+ disabled={!selectedTreatment || !selectedSlotKey || submitting}
184
+ className="w-full mt-6 inline-flex items-center justify-center gap-2 px-5 py-3 rounded-md bg-primary text-primary-foreground font-semibold text-sm hover:bg-primary/90 transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
185
+ >
186
+ {submitting
187
+ ? "Confirming…"
188
+ : selectedSlotKey
189
+ ? `Book ${selectedTreatment?.name} at ${new Date(selectedSlotKey).toLocaleTimeString(undefined, { hour: "2-digit", minute: "2-digit" })}`
190
+ : "Pick a slot to book"}
191
+ </button>
192
+ </div>
193
+ </div>
194
+ );
195
+ }
@@ -0,0 +1,65 @@
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 { BookClient } from "./book-client";
6
+ import { brand } from "@/lib/brand";
7
+
8
+ export const metadata: Metadata = {
9
+ title: `Book a treatment — ${brand.name}`,
10
+ description: "Pick a treatment, pick a slot, you're booked.",
11
+ };
12
+
13
+ async function getTreatments(): 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
+ // Booking flow only handles service-typed products.
22
+ return r.value.items.filter((p) => p.type === "service");
23
+ }
24
+
25
+ export default async function BookPage() {
26
+ const treatments = await getTreatments();
27
+ return (
28
+ <article className="max-w-5xl mx-auto px-6 sm:px-8 py-14">
29
+ <p className="text-[11px] font-mono uppercase tracking-[0.16em] text-primary mb-2">
30
+ Book a treatment
31
+ </p>
32
+ <h1 className="text-[clamp(2rem,5vw,3rem)] font-semibold mb-3 -tracking-[0.02em]">
33
+ Pick a treatment.<br />Pick a slot.
34
+ </h1>
35
+ <p className="text-muted-foreground leading-relaxed mb-10 max-w-xl">
36
+ Available windows for the next two weeks. Confirmation by SMS within a minute of
37
+ booking. Free cancellation up to 24 hours before.
38
+ </p>
39
+
40
+ <Suspense fallback={<BookSkeleton />}>
41
+ <BookClient treatments={treatments} />
42
+ </Suspense>
43
+ </article>
44
+ );
45
+ }
46
+
47
+ function BookSkeleton() {
48
+ return (
49
+ <div className="grid grid-cols-1 lg:grid-cols-[1fr_1.4fr] gap-8">
50
+ <div className="space-y-2">
51
+ {Array.from({ length: 6 }).map((_, i) => (
52
+ <div key={i} className="h-16 bg-muted rounded-2xl animate-pulse" />
53
+ ))}
54
+ </div>
55
+ <div className="rounded-2xl border border-border bg-card p-8">
56
+ <div className="h-5 w-40 bg-muted rounded mb-4 animate-pulse" />
57
+ <div className="grid grid-cols-3 gap-2">
58
+ {Array.from({ length: 9 }).map((_, i) => (
59
+ <div key={i} className="h-10 bg-muted rounded animate-pulse" />
60
+ ))}
61
+ </div>
62
+ </div>
63
+ </div>
64
+ );
65
+ }