@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,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: `Create an account — ${brand.name}`,
|
|
7
|
+
description: brand.account.signupSubtitle,
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Cimplify Link handles sign-up inside the `<CimplifyAccount>` iframe.
|
|
12
|
+
* We bounce /signup to /account; the iframe shows the create-account UI
|
|
13
|
+
* for visitors with no session.
|
|
14
|
+
*/
|
|
15
|
+
export default function SignupPage(): never {
|
|
16
|
+
redirect("/account");
|
|
17
|
+
}
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
import type { Metadata } from "next";
|
|
2
|
+
import Link from "next/link";
|
|
3
|
+
import { cacheTag, cacheLife } from "next/cache";
|
|
4
|
+
import { getServerClient, tags, type Product } from "@cimplify/sdk/server";
|
|
5
|
+
import { brand } from "@/lib/brand";
|
|
6
|
+
|
|
7
|
+
export const metadata: Metadata = {
|
|
8
|
+
title: `Sitemap — ${brand.name}`,
|
|
9
|
+
description: "A human-readable index of every page on this site.",
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
interface SitemapData {
|
|
13
|
+
products: { slug: string; name: string }[];
|
|
14
|
+
categories: { slug: string; name: string }[];
|
|
15
|
+
collections: { slug: string; name: string }[];
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
async function getSitemap(): Promise<SitemapData> {
|
|
19
|
+
"use cache";
|
|
20
|
+
cacheTag(tags.products(), tags.categories(), tags.collections());
|
|
21
|
+
cacheLife("hours");
|
|
22
|
+
|
|
23
|
+
const client = getServerClient();
|
|
24
|
+
const [pRes, cRes, colRes] = await Promise.all([
|
|
25
|
+
client.catalogue.getProducts({ limit: 500 }),
|
|
26
|
+
client.catalogue.getCategories(),
|
|
27
|
+
client.catalogue.getCollections(),
|
|
28
|
+
]);
|
|
29
|
+
return {
|
|
30
|
+
products: (pRes.ok ? pRes.value.items : []).map((p: Product) => ({
|
|
31
|
+
slug: p.slug ?? p.id,
|
|
32
|
+
name: p.name,
|
|
33
|
+
})),
|
|
34
|
+
categories: (cRes.ok ? cRes.value : []).map((c) => ({ slug: c.slug, name: c.name })),
|
|
35
|
+
collections: (colRes.ok ? colRes.value : []).map((c) => ({ slug: c.slug, name: c.name })),
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const STATIC_LINKS: { title: string; links: { href: string; label: string }[] }[] = [
|
|
40
|
+
{
|
|
41
|
+
title: "Browse",
|
|
42
|
+
links: [
|
|
43
|
+
{ href: "/", label: "Home" },
|
|
44
|
+
{ href: "/shop", label: "Shop" },
|
|
45
|
+
{ href: "/search", label: "Search" },
|
|
46
|
+
],
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
title: "Account",
|
|
50
|
+
links: [
|
|
51
|
+
{ href: "/account", label: "Account" },
|
|
52
|
+
{ href: "/account/orders", label: "Orders" },
|
|
53
|
+
{ href: "/account/addresses", label: "Addresses" },
|
|
54
|
+
{ href: "/account/settings", label: "Settings" },
|
|
55
|
+
{ href: "/login", label: "Sign in" },
|
|
56
|
+
{ href: "/signup", label: "Create account" },
|
|
57
|
+
{ href: "/track-order", label: "Track an order" },
|
|
58
|
+
{ href: "/cart", label: "Cart" },
|
|
59
|
+
{ href: "/checkout", label: "Checkout" },
|
|
60
|
+
],
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
title: "About",
|
|
64
|
+
links: [
|
|
65
|
+
{ href: "/about", label: "About" },
|
|
66
|
+
{ href: "/faq", label: "FAQ" },
|
|
67
|
+
{ href: "/contact", label: "Contact" },
|
|
68
|
+
],
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
title: "Policies",
|
|
72
|
+
links: [
|
|
73
|
+
{ href: "/shipping", label: "Shipping" },
|
|
74
|
+
{ href: "/returns", label: "Returns" },
|
|
75
|
+
{ href: "/accessibility", label: "Accessibility" },
|
|
76
|
+
{ href: "/terms", label: "Terms of Service" },
|
|
77
|
+
{ href: "/privacy", label: "Privacy Policy" },
|
|
78
|
+
],
|
|
79
|
+
},
|
|
80
|
+
{
|
|
81
|
+
title: "Machine-readable",
|
|
82
|
+
links: [
|
|
83
|
+
{ href: "/sitemap.xml", label: "sitemap.xml (search engines)" },
|
|
84
|
+
{ href: "/llms.txt", label: "llms.txt (LLM agents)" },
|
|
85
|
+
{ href: "/robots.txt", label: "robots.txt" },
|
|
86
|
+
{ href: "/opensearch.xml", label: "opensearch.xml (browser search)" },
|
|
87
|
+
],
|
|
88
|
+
},
|
|
89
|
+
];
|
|
90
|
+
|
|
91
|
+
export default async function SitemapHtmlPage() {
|
|
92
|
+
const { products, categories, collections } = await getSitemap();
|
|
93
|
+
|
|
94
|
+
return (
|
|
95
|
+
<article className="max-w-5xl mx-auto px-6 sm:px-8 py-16">
|
|
96
|
+
<p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-primary mb-2">
|
|
97
|
+
Sitemap
|
|
98
|
+
</p>
|
|
99
|
+
<h1 className="text-[clamp(2rem,4vw,2.75rem)] font-semibold mb-2 -tracking-[0.02em]">
|
|
100
|
+
Every page, in one place.
|
|
101
|
+
</h1>
|
|
102
|
+
<p className="text-muted-foreground mb-12">
|
|
103
|
+
For search engines, see <Link href="/sitemap.xml" className="text-primary hover:underline">/sitemap.xml</Link>.
|
|
104
|
+
For LLM agents, see <Link href="/llms.txt" className="text-primary hover:underline">/llms.txt</Link>.
|
|
105
|
+
</p>
|
|
106
|
+
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-10">
|
|
107
|
+
{STATIC_LINKS.map((s) => (
|
|
108
|
+
<Section key={s.title} title={s.title}>
|
|
109
|
+
{s.links.map((l) => (
|
|
110
|
+
<li key={l.href}>
|
|
111
|
+
<Link href={l.href} className="hover:text-primary transition-colors">
|
|
112
|
+
{l.label}
|
|
113
|
+
</Link>
|
|
114
|
+
</li>
|
|
115
|
+
))}
|
|
116
|
+
</Section>
|
|
117
|
+
))}
|
|
118
|
+
{categories.length > 0 && (
|
|
119
|
+
<Section title="Categories">
|
|
120
|
+
{categories.map((c) => (
|
|
121
|
+
<li key={c.slug}>
|
|
122
|
+
<Link href={`/categories/${c.slug}`} className="hover:text-primary transition-colors">
|
|
123
|
+
{c.name}
|
|
124
|
+
</Link>
|
|
125
|
+
</li>
|
|
126
|
+
))}
|
|
127
|
+
</Section>
|
|
128
|
+
)}
|
|
129
|
+
{collections.length > 0 && (
|
|
130
|
+
<Section title="Collections">
|
|
131
|
+
{collections.map((c) => (
|
|
132
|
+
<li key={c.slug}>
|
|
133
|
+
<Link href={`/collections/${c.slug}`} className="hover:text-primary transition-colors">
|
|
134
|
+
{c.name}
|
|
135
|
+
</Link>
|
|
136
|
+
</li>
|
|
137
|
+
))}
|
|
138
|
+
</Section>
|
|
139
|
+
)}
|
|
140
|
+
{products.length > 0 && (
|
|
141
|
+
<Section title={`Products (${products.length})`}>
|
|
142
|
+
{products.map((p) => (
|
|
143
|
+
<li key={p.slug}>
|
|
144
|
+
<Link href={`/shop?product=${encodeURIComponent(p.slug)}`} className="hover:text-primary transition-colors">
|
|
145
|
+
{p.name}
|
|
146
|
+
</Link>
|
|
147
|
+
</li>
|
|
148
|
+
))}
|
|
149
|
+
</Section>
|
|
150
|
+
)}
|
|
151
|
+
</div>
|
|
152
|
+
</article>
|
|
153
|
+
);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function Section({ title, children }: { title: string; children: React.ReactNode }) {
|
|
157
|
+
return (
|
|
158
|
+
<div>
|
|
159
|
+
<p className="font-semibold text-[12px] uppercase tracking-[0.12em] text-foreground mb-3">
|
|
160
|
+
{title}
|
|
161
|
+
</p>
|
|
162
|
+
<ul className="space-y-2 m-0 p-0 list-none text-sm text-muted-foreground">
|
|
163
|
+
{children}
|
|
164
|
+
</ul>
|
|
165
|
+
</div>
|
|
166
|
+
);
|
|
167
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { Metadata } from "next";
|
|
2
|
+
import { TrackOrderForm } from "./track-order-form";
|
|
3
|
+
import { brand } from "@/lib/brand";
|
|
4
|
+
|
|
5
|
+
export const metadata: Metadata = {
|
|
6
|
+
title: `Track an order — ${brand.name}`,
|
|
7
|
+
description: brand.trackOrder.body,
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
export default function TrackOrderPage() {
|
|
11
|
+
const t = brand.trackOrder;
|
|
12
|
+
return (
|
|
13
|
+
<article className="max-w-2xl 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
|
+
{t.eyebrow}
|
|
16
|
+
</p>
|
|
17
|
+
<h1 className="font-serif text-[clamp(2rem,5vw,3rem)] font-semibold mb-4 -tracking-[0.02em]">
|
|
18
|
+
{t.title}
|
|
19
|
+
</h1>
|
|
20
|
+
<p className="text-muted-foreground leading-relaxed mb-8">{t.body}</p>
|
|
21
|
+
<TrackOrderForm />
|
|
22
|
+
</article>
|
|
23
|
+
);
|
|
24
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useState } from "react";
|
|
4
|
+
import { useRouter } from "next/navigation";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Guest order tracker. Routes /orders/<id> with the entered id; the
|
|
8
|
+
* post-checkout confirmation page already lives at /orders/[id] so this
|
|
9
|
+
* just sends the visitor there. Replace the redirect with a real lookup
|
|
10
|
+
* (e.g. Cimplify orders API + email-match guard) for production.
|
|
11
|
+
*/
|
|
12
|
+
export function TrackOrderForm() {
|
|
13
|
+
const router = useRouter();
|
|
14
|
+
const [orderId, setOrderId] = useState("");
|
|
15
|
+
const [email, setEmail] = useState("");
|
|
16
|
+
|
|
17
|
+
return (
|
|
18
|
+
<form
|
|
19
|
+
onSubmit={(e) => {
|
|
20
|
+
e.preventDefault();
|
|
21
|
+
if (!orderId.trim()) return;
|
|
22
|
+
router.push(`/orders/${encodeURIComponent(orderId.trim())}`);
|
|
23
|
+
}}
|
|
24
|
+
className="space-y-4 rounded-2xl border border-border bg-card p-6"
|
|
25
|
+
>
|
|
26
|
+
<div>
|
|
27
|
+
<label
|
|
28
|
+
htmlFor="orderId"
|
|
29
|
+
className="text-xs font-semibold uppercase tracking-wider text-muted-foreground block mb-1.5"
|
|
30
|
+
>
|
|
31
|
+
Order number
|
|
32
|
+
</label>
|
|
33
|
+
<input
|
|
34
|
+
id="orderId"
|
|
35
|
+
name="orderId"
|
|
36
|
+
required
|
|
37
|
+
value={orderId}
|
|
38
|
+
onChange={(e) => setOrderId(e.target.value)}
|
|
39
|
+
placeholder="ord_abc123…"
|
|
40
|
+
className="w-full px-4 py-3 rounded-md bg-background border border-border focus:border-primary focus:ring-2 focus:ring-primary/20 outline-none text-sm"
|
|
41
|
+
/>
|
|
42
|
+
</div>
|
|
43
|
+
<div>
|
|
44
|
+
<label
|
|
45
|
+
htmlFor="email"
|
|
46
|
+
className="text-xs font-semibold uppercase tracking-wider text-muted-foreground block mb-1.5"
|
|
47
|
+
>
|
|
48
|
+
Order email
|
|
49
|
+
</label>
|
|
50
|
+
<input
|
|
51
|
+
id="email"
|
|
52
|
+
name="email"
|
|
53
|
+
type="email"
|
|
54
|
+
required
|
|
55
|
+
value={email}
|
|
56
|
+
onChange={(e) => setEmail(e.target.value)}
|
|
57
|
+
placeholder="you@email.com"
|
|
58
|
+
className="w-full px-4 py-3 rounded-md bg-background border border-border focus:border-primary focus:ring-2 focus:ring-primary/20 outline-none text-sm"
|
|
59
|
+
/>
|
|
60
|
+
</div>
|
|
61
|
+
<button
|
|
62
|
+
type="submit"
|
|
63
|
+
className="w-full 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"
|
|
64
|
+
>
|
|
65
|
+
Track order
|
|
66
|
+
</button>
|
|
67
|
+
</form>
|
|
68
|
+
);
|
|
69
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { CimplifyAccount } from "@cimplify/sdk/react";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Cimplify Account portal — iframe-mounted UI hosted by Cimplify Link.
|
|
7
|
+
* Handles sign-in, sign-up, OTP, addresses, payment methods, sessions,
|
|
8
|
+
* and order history. The iframe owns auth state; we just choose which
|
|
9
|
+
* `section` to land on.
|
|
10
|
+
*/
|
|
11
|
+
export function AccountIframe({ section }: { section?: string }) {
|
|
12
|
+
return <CimplifyAccount section={section} />;
|
|
13
|
+
}
|
|
@@ -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
|
+
}
|
|
@@ -58,6 +58,20 @@ export interface Brand {
|
|
|
58
58
|
};
|
|
59
59
|
terms: { eyebrow: string; title: string; lastUpdated: string; sections: BrandPolicySection[] };
|
|
60
60
|
privacy: { eyebrow: string; title: string; lastUpdated: string; sections: BrandPolicySection[] };
|
|
61
|
+
shipping: { eyebrow: string; title: string; lastUpdated: string; sections: BrandPolicySection[] };
|
|
62
|
+
returns: { eyebrow: string; title: string; lastUpdated: string; sections: BrandPolicySection[] };
|
|
63
|
+
accessibility: { eyebrow: string; title: string; lastUpdated: string; sections: BrandPolicySection[] };
|
|
64
|
+
account: {
|
|
65
|
+
loginEyebrow: string; loginTitle: string; loginSubtitle: string;
|
|
66
|
+
signupEyebrow: string; signupTitle: string; signupSubtitle: string;
|
|
67
|
+
accountEyebrow: string; accountTitle: string;
|
|
68
|
+
};
|
|
69
|
+
contactPage: {
|
|
70
|
+
eyebrow: string; title: string; body: string;
|
|
71
|
+
reasons: string[];
|
|
72
|
+
directLines: { label: string; value: string; href: string }[];
|
|
73
|
+
};
|
|
74
|
+
trackOrder: { eyebrow: string; title: string; body: string };
|
|
61
75
|
footer: {
|
|
62
76
|
blurb: string; sitemap: BrandSitemapSection[];
|
|
63
77
|
poweredBy?: { label: string; href: string };
|
|
@@ -232,6 +246,68 @@ export const brand: Brand = {
|
|
|
232
246
|
],
|
|
233
247
|
},
|
|
234
248
|
|
|
249
|
+
shipping: {
|
|
250
|
+
eyebrow: "Delivery",
|
|
251
|
+
title: "Delivery & Pickup",
|
|
252
|
+
lastUpdated: "1 May 2026",
|
|
253
|
+
sections: [
|
|
254
|
+
{ heading: "Greater Accra", body: "Same-day, 12pm–9pm Tue–Sun. Free over GH₵250; GH₵35 below. Typical 60-minute window from order." },
|
|
255
|
+
{ heading: "Pickup", body: "Skip the queue: order ahead, pick a window, walk in. Free." },
|
|
256
|
+
{ heading: "Catering & private events", body: "Off-site catering (12+ people) requires 5 days' notice. Email events@mamaskitchen.test." },
|
|
257
|
+
],
|
|
258
|
+
},
|
|
259
|
+
|
|
260
|
+
returns: {
|
|
261
|
+
eyebrow: "Refunds",
|
|
262
|
+
title: "Refunds & Quality",
|
|
263
|
+
lastUpdated: "1 May 2026",
|
|
264
|
+
sections: [
|
|
265
|
+
{ heading: "Quality guarantee", body: "If anything is wrong with your order — wrong dish, cold, off — message us within 2 hours and we'll replace, refund, or credit." },
|
|
266
|
+
{ heading: "Reservation cancellations", body: "Free up to 4 hours before. Within 4 hours, parties of 6+ are charged GH₵60/cover for no-shows." },
|
|
267
|
+
{ heading: "Custom event deposits", body: "Refundable up to 7 days before the event. Within 7 days, deposits are non-refundable." },
|
|
268
|
+
],
|
|
269
|
+
},
|
|
270
|
+
|
|
271
|
+
accessibility: {
|
|
272
|
+
eyebrow: "Accessibility",
|
|
273
|
+
title: "Accessibility Statement",
|
|
274
|
+
lastUpdated: "1 May 2026",
|
|
275
|
+
sections: [
|
|
276
|
+
{ heading: "In the dining room", body: "Step-free entrance, accessible bathroom, and a flat path from the curb. Service dogs welcome. We can move tables in advance — let us know at booking." },
|
|
277
|
+
{ heading: "Online", body: "We aim for WCAG 2.1 AA on the website and test it on every release." },
|
|
278
|
+
{ heading: "Reporting issues", body: "Email accessibility@mamaskitchen.test or call ahead — we respond within a business day." },
|
|
279
|
+
],
|
|
280
|
+
},
|
|
281
|
+
|
|
282
|
+
account: {
|
|
283
|
+
loginEyebrow: "Welcome back",
|
|
284
|
+
loginTitle: "Sign in to Mama's",
|
|
285
|
+
loginSubtitle: "Pick up where you left off — orders, reservations, and your favourites.",
|
|
286
|
+
signupEyebrow: "Welcome",
|
|
287
|
+
signupTitle: "Create an account",
|
|
288
|
+
signupSubtitle: "Reorder in one tap, hold tables faster, and get first-look at the long-table evenings.",
|
|
289
|
+
accountEyebrow: "Your account",
|
|
290
|
+
accountTitle: "Welcome back",
|
|
291
|
+
},
|
|
292
|
+
|
|
293
|
+
contactPage: {
|
|
294
|
+
eyebrow: "Contact",
|
|
295
|
+
title: "Talk to the kitchen.",
|
|
296
|
+
body: "Reservation question, dietary need, private event — message us and we'll reply within a business day.",
|
|
297
|
+
reasons: ["Reservation question", "An order issue", "Dietary / allergens", "Private event", "Press / partnerships", "Something else"],
|
|
298
|
+
directLines: [
|
|
299
|
+
{ label: "Email", value: "hello@mamaskitchen.test", href: "mailto:hello@mamaskitchen.test" },
|
|
300
|
+
{ label: "Phone", value: "+233 244 200 100", href: "tel:+233244200100" },
|
|
301
|
+
{ label: "WhatsApp", value: "+233 244 200 100", href: "https://wa.me/233244200100" },
|
|
302
|
+
],
|
|
303
|
+
},
|
|
304
|
+
|
|
305
|
+
trackOrder: {
|
|
306
|
+
eyebrow: "Track an order",
|
|
307
|
+
title: "Where's my order?",
|
|
308
|
+
body: "Enter your order number and the email used at checkout. We'll show the kitchen status, the rider, and the live ETA.",
|
|
309
|
+
},
|
|
310
|
+
|
|
235
311
|
footer: {
|
|
236
312
|
blurb:
|
|
237
313
|
"West African plates, prepared by our matriarchs, served family-style in East Legon. Dine in, take out, deliver.",
|
|
@@ -258,10 +334,20 @@ export const brand: Brand = {
|
|
|
258
334
|
{
|
|
259
335
|
title: "Help",
|
|
260
336
|
links: [
|
|
261
|
-
{ label: "Contact", href: "
|
|
337
|
+
{ label: "Contact", href: "/contact" },
|
|
338
|
+
{ label: "Track an order", href: "/track-order" },
|
|
339
|
+
{ label: "Delivery", href: "/shipping" },
|
|
340
|
+
{ label: "Refunds", href: "/returns" },
|
|
262
341
|
{ label: "FAQ", href: "/faq" },
|
|
263
|
-
|
|
264
|
-
|
|
342
|
+
],
|
|
343
|
+
},
|
|
344
|
+
{
|
|
345
|
+
title: "Account",
|
|
346
|
+
links: [
|
|
347
|
+
{ label: "Sign in", href: "/login" },
|
|
348
|
+
{ label: "Create account", href: "/signup" },
|
|
349
|
+
{ label: "Your orders", href: "/account/orders" },
|
|
350
|
+
{ label: "Settings", href: "/account/settings" },
|
|
265
351
|
],
|
|
266
352
|
},
|
|
267
353
|
{
|
|
@@ -269,6 +355,8 @@ export const brand: Brand = {
|
|
|
269
355
|
links: [
|
|
270
356
|
{ label: "Terms of Service", href: "/terms" },
|
|
271
357
|
{ label: "Privacy Policy", href: "/privacy" },
|
|
358
|
+
{ label: "Accessibility", href: "/accessibility" },
|
|
359
|
+
{ label: "Sitemap", href: "/sitemap-page" },
|
|
272
360
|
],
|
|
273
361
|
},
|
|
274
362
|
],
|
|
@@ -4,39 +4,75 @@ If you are an AI agent (Claude, Cursor, Aider, devin, …) working on this store
|
|
|
4
4
|
|
|
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`** — `@theme { … }` block for palette + radius + font references.
|
|
9
|
+
3. **Edit `.env.local`** — `NEXT_PUBLIC_CIMPLIFY_API_URL`, `NEXT_PUBLIC_CIMPLIFY_PUBLIC_KEY`, `NEXT_PUBLIC_CIMPLIFY_BUSINESS_ID`, `NEXT_PUBLIC_SITE_URL`.
|
|
8
10
|
|
|
9
|
-
|
|
10
|
-
2. **Edit `app/globals.css`** — the `@theme { … }` block holds the design tokens (palette, radius, font references). Tweak to match the merchant's brand.
|
|
11
|
-
3. **Edit `.env.local`** — set `NEXT_PUBLIC_CIMPLIFY_API_URL`, `NEXT_PUBLIC_CIMPLIFY_PUBLIC_KEY`, `NEXT_PUBLIC_CIMPLIFY_BUSINESS_ID`, `NEXT_PUBLIC_SITE_URL`.
|
|
11
|
+
## Aesthetic
|
|
12
12
|
|
|
13
|
-
|
|
13
|
+
- **Inter + JetBrains Mono** — clean modern sans, mono-spaced labels for spec-feel.
|
|
14
|
+
- **Cool navy + electric blue**: dark foreground, electric primary.
|
|
15
|
+
- **Sharp corners**: `0.5rem` — modern retail.
|
|
16
|
+
- **Section heroes** in dark with mono-spaced eyebrow labels.
|
|
14
17
|
|
|
15
|
-
##
|
|
18
|
+
## Page surface
|
|
16
19
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
-
|
|
20
|
-
|
|
21
|
-
-
|
|
22
|
-
|
|
23
|
-
|
|
20
|
+
```
|
|
21
|
+
app/
|
|
22
|
+
page.tsx Multi-section home — split hero, trust bar, category tiles,
|
|
23
|
+
promo banner, "Just dropped" grid, brand strip, collections,
|
|
24
|
+
trade-in CTA, best sellers, newsletter
|
|
25
|
+
shop/page.tsx SDK <CataloguePage/> with custom hero
|
|
26
|
+
search/page.tsx Search (SDK <SearchPage/>)
|
|
27
|
+
collections/[slug]/page.tsx Collection landing
|
|
28
|
+
categories/[slug]/page.tsx Category landing
|
|
29
|
+
products/[slug]/page.tsx Full product detail page (Product JSON-LD)
|
|
30
|
+
|
|
31
|
+
cart/page.tsx SDK <CartPage/>
|
|
32
|
+
checkout/page.tsx SDK <CheckoutPage/>
|
|
33
|
+
orders/[id]/page.tsx Post-checkout confirmation
|
|
34
|
+
|
|
35
|
+
account/page.tsx <CimplifyAccount /> (iframe)
|
|
36
|
+
account/orders/page.tsx <CimplifyAccount section="orders" />
|
|
37
|
+
account/addresses/page.tsx <CimplifyAccount section="addresses" />
|
|
38
|
+
account/settings/page.tsx <CimplifyAccount section="settings" />
|
|
39
|
+
login/page.tsx, signup/page.tsx redirects → /account
|
|
40
|
+
|
|
41
|
+
contact/page.tsx Branded contact form
|
|
42
|
+
track-order/page.tsx Guest lookup → /orders/[id]
|
|
43
|
+
|
|
44
|
+
about/page.tsx, faq/page.tsx Brand pages
|
|
45
|
+
shipping/page.tsx, returns/page.tsx, accessibility/page.tsx Standalone policies
|
|
46
|
+
terms/page.tsx, privacy/page.tsx
|
|
47
|
+
|
|
48
|
+
sitemap-page/page.tsx HTML sitemap
|
|
49
|
+
sitemap.ts, robots.ts XML sitemap, robots.txt
|
|
50
|
+
llms.txt/route.ts LLM-friendly site index
|
|
51
|
+
opensearch.xml/route.ts Browser search description
|
|
52
|
+
error.tsx, not-found.tsx Global boundaries
|
|
53
|
+
```
|
|
24
54
|
|
|
25
|
-
## File
|
|
55
|
+
## File ↔ brand-field map
|
|
26
56
|
|
|
27
57
|
| File | Reads from `brand` |
|
|
28
58
|
|---|---|
|
|
29
|
-
| `app/layout.tsx` |
|
|
59
|
+
| `app/layout.tsx` | identity, contact, socials, schemaType (Store JSON-LD) |
|
|
30
60
|
| `app/page.tsx` | `brand.hero`, `brand.trustItems`, `brand.brandStrip`, `brand.promo`, `brand.tradeIn`, `brand.newsletter` |
|
|
31
61
|
| `app/about/page.tsx` | `brand.about` |
|
|
32
62
|
| `app/faq/page.tsx` | `brand.faq` |
|
|
33
63
|
| `app/terms/page.tsx` | `brand.terms` |
|
|
34
64
|
| `app/privacy/page.tsx` | `brand.privacy` |
|
|
35
|
-
| `app/
|
|
65
|
+
| `app/shipping/page.tsx` | `brand.shipping` (warranty + returns + same-day delivery copy) |
|
|
66
|
+
| `app/returns/page.tsx` | `brand.returns` |
|
|
67
|
+
| `app/accessibility/page.tsx` | `brand.accessibility` |
|
|
68
|
+
| `app/contact/page.tsx` | `brand.contactPage`, `brand.contact` |
|
|
69
|
+
| `app/track-order/page.tsx` | `brand.trackOrder` |
|
|
70
|
+
| `app/account/*/page.tsx` | `brand.account` (Cimplify Link iframe owns the UI) |
|
|
71
|
+
| `app/products/[slug]/page.tsx` | `brand.name`, `brand.currency` (Product JSON-LD: brand, offer, availability) |
|
|
36
72
|
| `app/llms.txt/route.ts` | `brand.llms`, contact, currency |
|
|
37
|
-
| `app/
|
|
73
|
+
| `app/opensearch.xml/route.ts` | `brand.shortName`, `brand.name` |
|
|
38
74
|
| `components/header.tsx` | `brand.shortName`, `brand.microTag`, `brand.header.nav` |
|
|
39
|
-
| `components/footer.tsx` | `brand.footer`, contact
|
|
75
|
+
| `components/footer.tsx` | `brand.footer`, `brand.contact`, `brand.socials` |
|
|
40
76
|
| `components/promo-banner.tsx` | `brand.promo` (renders nothing if absent) |
|
|
41
77
|
| `components/trade-in-cta.tsx` | `brand.tradeIn` (renders nothing if absent) |
|
|
42
78
|
| `components/brand-marquee.tsx` | `brand.brandStrip` |
|
|
@@ -45,20 +81,25 @@ That is the entire rebrand. Do not modify any `.tsx` file in `app/` or `componen
|
|
|
45
81
|
|
|
46
82
|
## Retail-specific notes
|
|
47
83
|
|
|
48
|
-
- Product detail is a **full `/products/[slug]` page** with image gallery, customizer, related products rail, and Product JSON-LD.
|
|
49
|
-
- Schema.org type is `Store`.
|
|
50
|
-
- Optional sections render conditionally: `brand.promo`, `brand.tradeIn`, `brand.trustItems`, `brand.brandStrip`
|
|
51
|
-
- `app/page.tsx` uses Unsplash fallback hero
|
|
84
|
+
- Product detail is a **full `/products/[slug]` page** with image gallery, customizer, related products rail, and Product JSON-LD. Consideration purchases (phones, laptops, headphones) need real estate. Don't replace with a modal.
|
|
85
|
+
- Schema.org `@type` is `Store`.
|
|
86
|
+
- Optional sections render conditionally: set `brand.promo`, `brand.tradeIn`, `brand.trustItems`, or `brand.brandStrip` to `undefined` to hide them.
|
|
87
|
+
- `app/page.tsx` uses an Unsplash fallback hero (`HERO_FALLBACK_IMAGE`) — replace with the merchant's own product photography.
|
|
88
|
+
|
|
89
|
+
## Known TODOs
|
|
90
|
+
|
|
91
|
+
- Contact form + newsletter signup currently fake their submit. Wire `client.support.sendMessage(...)` for contact and a real list provider for newsletter.
|
|
92
|
+
- Trust-bar / brand-marquee imagery / wordmarks are placeholders — swap for the merchant's real partners.
|
|
52
93
|
|
|
53
94
|
## Mock seed
|
|
54
95
|
|
|
55
|
-
Wired to `--seed retail` (Currents Electronics)
|
|
96
|
+
Wired to `--seed retail` (Currents Electronics). Edit `dev:mock` in `package.json` to preview another seed.
|
|
56
97
|
|
|
57
98
|
## Quick start
|
|
58
99
|
|
|
59
100
|
```bash
|
|
60
101
|
bun install
|
|
61
|
-
bun dev
|
|
102
|
+
bun dev
|
|
62
103
|
```
|
|
63
104
|
|
|
64
105
|
Open <http://localhost:3000>.
|
|
@@ -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
|
+
}
|