@cimplify/sdk 0.41.0 → 0.42.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli/commands/deploy.mjs +561 -0
- package/dist/cli/commands/dev.mjs +434 -0
- package/dist/cli/commands/domains.mjs +479 -0
- package/dist/cli/commands/env.mjs +610 -0
- package/dist/cli/commands/link.mjs +325 -0
- package/dist/cli/commands/login.mjs +515 -0
- package/dist/cli/commands/logout.mjs +54 -0
- package/dist/cli/commands/logs.mjs +358 -0
- package/dist/cli/commands/projects.mjs +327 -0
- package/dist/cli/commands/rollback.mjs +477 -0
- package/dist/cli/commands/status.mjs +380 -0
- package/dist/cli/commands/unlink.mjs +58 -0
- package/dist/cli/commands/whoami.mjs +234 -0
- package/dist/dispatcher.mjs +91 -0
- package/dist/styles.css +1 -1
- package/package.json +1 -1
- package/templates/storefront-bakery/app/accessibility/page.tsx +11 -0
- package/templates/storefront-bakery/app/account/addresses/page.tsx +21 -0
- package/templates/storefront-bakery/app/account/orders/page.tsx +21 -0
- package/templates/storefront-bakery/app/account/page.tsx +22 -0
- package/templates/storefront-bakery/app/account/settings/page.tsx +21 -0
- package/templates/storefront-bakery/app/contact/contact-form.tsx +109 -0
- package/templates/storefront-bakery/app/contact/page.tsx +54 -0
- package/templates/storefront-bakery/app/login/page.tsx +17 -0
- package/templates/storefront-bakery/app/opensearch.xml/route.ts +37 -0
- package/templates/storefront-bakery/app/returns/page.tsx +11 -0
- package/templates/storefront-bakery/app/search/page.tsx +38 -0
- package/templates/storefront-bakery/app/search/search-client.tsx +7 -0
- package/templates/storefront-bakery/app/shipping/page.tsx +16 -0
- package/templates/storefront-bakery/app/signup/page.tsx +17 -0
- package/templates/storefront-bakery/app/sitemap-page/page.tsx +167 -0
- package/templates/storefront-bakery/app/track-order/page.tsx +24 -0
- package/templates/storefront-bakery/app/track-order/track-order-form.tsx +69 -0
- package/templates/storefront-bakery/components/account-iframe.tsx +13 -0
- package/templates/storefront-bakery/components/policy-page.tsx +49 -0
- package/templates/storefront-bakery/lib/brand.ts +119 -4
- package/templates/storefront-fashion/app/accessibility/page.tsx +11 -0
- package/templates/storefront-fashion/app/account/addresses/page.tsx +21 -0
- package/templates/storefront-fashion/app/account/orders/page.tsx +21 -0
- package/templates/storefront-fashion/app/account/page.tsx +22 -0
- package/templates/storefront-fashion/app/account/settings/page.tsx +21 -0
- package/templates/storefront-fashion/app/contact/contact-form.tsx +109 -0
- package/templates/storefront-fashion/app/contact/page.tsx +54 -0
- package/templates/storefront-fashion/app/login/page.tsx +17 -0
- package/templates/storefront-fashion/app/lookbook/page.tsx +132 -0
- package/templates/storefront-fashion/app/opensearch.xml/route.ts +37 -0
- package/templates/storefront-fashion/app/returns/page.tsx +11 -0
- package/templates/storefront-fashion/app/search/page.tsx +38 -0
- package/templates/storefront-fashion/app/search/search-client.tsx +7 -0
- package/templates/storefront-fashion/app/shipping/page.tsx +16 -0
- package/templates/storefront-fashion/app/signup/page.tsx +17 -0
- package/templates/storefront-fashion/app/sitemap-page/page.tsx +167 -0
- package/templates/storefront-fashion/app/size-guide/page.tsx +155 -0
- package/templates/storefront-fashion/app/track-order/page.tsx +24 -0
- package/templates/storefront-fashion/app/track-order/track-order-form.tsx +69 -0
- package/templates/storefront-fashion/components/account-iframe.tsx +13 -0
- package/templates/storefront-fashion/components/policy-page.tsx +49 -0
- package/templates/storefront-fashion/lib/brand.ts +99 -4
- package/templates/storefront-grocery/app/accessibility/page.tsx +11 -0
- package/templates/storefront-grocery/app/account/addresses/page.tsx +21 -0
- package/templates/storefront-grocery/app/account/orders/page.tsx +21 -0
- package/templates/storefront-grocery/app/account/page.tsx +22 -0
- package/templates/storefront-grocery/app/account/settings/page.tsx +21 -0
- package/templates/storefront-grocery/app/contact/contact-form.tsx +109 -0
- package/templates/storefront-grocery/app/contact/page.tsx +54 -0
- package/templates/storefront-grocery/app/login/page.tsx +17 -0
- package/templates/storefront-grocery/app/opensearch.xml/route.ts +37 -0
- package/templates/storefront-grocery/app/returns/page.tsx +11 -0
- package/templates/storefront-grocery/app/search/page.tsx +38 -0
- package/templates/storefront-grocery/app/search/search-client.tsx +7 -0
- package/templates/storefront-grocery/app/shipping/page.tsx +16 -0
- package/templates/storefront-grocery/app/signup/page.tsx +17 -0
- package/templates/storefront-grocery/app/sitemap-page/page.tsx +167 -0
- package/templates/storefront-grocery/app/track-order/page.tsx +24 -0
- package/templates/storefront-grocery/app/track-order/track-order-form.tsx +69 -0
- package/templates/storefront-grocery/components/account-iframe.tsx +13 -0
- package/templates/storefront-grocery/components/policy-page.tsx +49 -0
- package/templates/storefront-grocery/lib/brand.ts +92 -3
- package/templates/storefront-restaurant/app/accessibility/page.tsx +11 -0
- package/templates/storefront-restaurant/app/account/addresses/page.tsx +21 -0
- package/templates/storefront-restaurant/app/account/orders/page.tsx +21 -0
- package/templates/storefront-restaurant/app/account/page.tsx +22 -0
- package/templates/storefront-restaurant/app/account/settings/page.tsx +21 -0
- package/templates/storefront-restaurant/app/contact/contact-form.tsx +109 -0
- package/templates/storefront-restaurant/app/contact/page.tsx +54 -0
- package/templates/storefront-restaurant/app/login/page.tsx +17 -0
- package/templates/storefront-restaurant/app/opensearch.xml/route.ts +37 -0
- package/templates/storefront-restaurant/app/reservations/page.tsx +66 -0
- package/templates/storefront-restaurant/app/reservations/reservations-client.tsx +234 -0
- package/templates/storefront-restaurant/app/returns/page.tsx +11 -0
- package/templates/storefront-restaurant/app/search/page.tsx +38 -0
- package/templates/storefront-restaurant/app/search/search-client.tsx +7 -0
- package/templates/storefront-restaurant/app/shipping/page.tsx +16 -0
- package/templates/storefront-restaurant/app/signup/page.tsx +17 -0
- package/templates/storefront-restaurant/app/sitemap-page/page.tsx +167 -0
- package/templates/storefront-restaurant/app/track-order/page.tsx +24 -0
- package/templates/storefront-restaurant/app/track-order/track-order-form.tsx +69 -0
- package/templates/storefront-restaurant/components/account-iframe.tsx +13 -0
- package/templates/storefront-restaurant/components/policy-page.tsx +49 -0
- package/templates/storefront-restaurant/lib/brand.ts +91 -3
- package/templates/storefront-retail/app/accessibility/page.tsx +11 -0
- package/templates/storefront-retail/app/account/addresses/page.tsx +21 -0
- package/templates/storefront-retail/app/account/orders/page.tsx +21 -0
- package/templates/storefront-retail/app/account/page.tsx +22 -0
- package/templates/storefront-retail/app/account/settings/page.tsx +21 -0
- package/templates/storefront-retail/app/contact/contact-form.tsx +109 -0
- package/templates/storefront-retail/app/contact/page.tsx +54 -0
- package/templates/storefront-retail/app/login/page.tsx +17 -0
- package/templates/storefront-retail/app/opensearch.xml/route.ts +37 -0
- package/templates/storefront-retail/app/returns/page.tsx +11 -0
- package/templates/storefront-retail/app/search/page.tsx +38 -0
- package/templates/storefront-retail/app/search/search-client.tsx +7 -0
- package/templates/storefront-retail/app/shipping/page.tsx +16 -0
- package/templates/storefront-retail/app/signup/page.tsx +17 -0
- package/templates/storefront-retail/app/sitemap-page/page.tsx +167 -0
- package/templates/storefront-retail/app/track-order/page.tsx +24 -0
- package/templates/storefront-retail/app/track-order/track-order-form.tsx +69 -0
- package/templates/storefront-retail/components/account-iframe.tsx +13 -0
- package/templates/storefront-retail/components/policy-page.tsx +49 -0
- package/templates/storefront-retail/lib/brand.ts +105 -9
- package/templates/storefront-services/app/accessibility/page.tsx +11 -0
- package/templates/storefront-services/app/account/addresses/page.tsx +21 -0
- package/templates/storefront-services/app/account/orders/page.tsx +21 -0
- package/templates/storefront-services/app/account/page.tsx +22 -0
- package/templates/storefront-services/app/account/settings/page.tsx +21 -0
- package/templates/storefront-services/app/book/book-client.tsx +195 -0
- package/templates/storefront-services/app/book/page.tsx +65 -0
- package/templates/storefront-services/app/contact/contact-form.tsx +109 -0
- package/templates/storefront-services/app/contact/page.tsx +54 -0
- package/templates/storefront-services/app/login/page.tsx +17 -0
- package/templates/storefront-services/app/opensearch.xml/route.ts +37 -0
- package/templates/storefront-services/app/returns/page.tsx +11 -0
- package/templates/storefront-services/app/search/page.tsx +38 -0
- package/templates/storefront-services/app/search/search-client.tsx +7 -0
- package/templates/storefront-services/app/shipping/page.tsx +16 -0
- package/templates/storefront-services/app/signup/page.tsx +17 -0
- package/templates/storefront-services/app/sitemap-page/page.tsx +167 -0
- package/templates/storefront-services/app/track-order/page.tsx +24 -0
- package/templates/storefront-services/app/track-order/track-order-form.tsx +69 -0
- package/templates/storefront-services/components/account-iframe.tsx +13 -0
- package/templates/storefront-services/components/policy-page.tsx +49 -0
- package/templates/storefront-services/lib/brand.ts +93 -2
|
@@ -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
|
],
|
|
@@ -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
|
+
}
|
|
@@ -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
|
+
}
|