@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.
- 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/AGENTS.md +61 -10
- 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/AGENTS.md +80 -26
- 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/AGENTS.md +56 -11
- 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/AGENTS.md +64 -13
- 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/AGENTS.md +65 -24
- 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/AGENTS.md +64 -16
- 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,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: "
|
|
533
|
-
{ label: "
|
|
534
|
-
{ label: "
|
|
535
|
-
{ label: "
|
|
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: "
|
|
633
|
+
title: "Account",
|
|
540
634
|
links: [
|
|
541
|
-
{ label: "
|
|
542
|
-
{ label: "
|
|
543
|
-
{ label: "
|
|
544
|
-
{ label: "
|
|
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`** —
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
24
|
-
-
|
|
25
|
-
-
|
|
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
|
-
##
|
|
77
|
+
## Known TODOs
|
|
31
78
|
|
|
32
|
-
|
|
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
|
+
}
|