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