@01.software/init 0.9.2 → 0.10.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/ai-docs.d.ts +13 -0
- package/dist/ai-docs.js +0 -0
- package/dist/browser-auth-CJDrpp5T.d.ts +11 -0
- package/dist/{chunk-UA7WNT2F.js → chunk-4LHYICUL.js} +1 -1
- package/dist/chunk-4LHYICUL.js.map +1 -0
- package/dist/{chunk-R4GGO33X.js → chunk-NJ4X7VNK.js} +1 -1
- package/dist/{chunk-R4GGO33X.js.map → chunk-NJ4X7VNK.js.map} +1 -1
- package/dist/chunk-Q6MSORYN.js +0 -0
- package/dist/chunk-STM4DKVZ.js +183 -0
- package/dist/chunk-STM4DKVZ.js.map +1 -0
- package/dist/{chunk-ENQSB4OF.js → chunk-WDWJ73KP.js} +40 -214
- package/dist/chunk-WDWJ73KP.js.map +1 -0
- package/dist/create-app-templates/ecommerce/AGENTS.md +88 -0
- package/dist/create-app-templates/ecommerce/CHANGELOG.md +30 -0
- package/dist/create-app-templates/ecommerce/CLAUDE.md +1 -0
- package/dist/create-app-templates/ecommerce/README.md +139 -0
- package/dist/create-app-templates/ecommerce/app/api/auth/login/route.ts +30 -0
- package/dist/create-app-templates/ecommerce/app/api/auth/logout/route.ts +18 -0
- package/dist/create-app-templates/ecommerce/app/api/auth/register/route.ts +41 -0
- package/dist/create-app-templates/ecommerce/app/api/cart/clear/route.ts +12 -0
- package/dist/create-app-templates/ecommerce/app/api/cart/items/route.ts +45 -0
- package/dist/create-app-templates/ecommerce/app/api/cart/route.ts +14 -0
- package/dist/create-app-templates/ecommerce/app/api/checkout/payment-return/route.ts +86 -0
- package/dist/create-app-templates/ecommerce/app/api/checkout/reconcile/route.ts +50 -0
- package/dist/create-app-templates/ecommerce/app/api/checkout/route.ts +41 -0
- package/dist/create-app-templates/ecommerce/app/cart/page.tsx +10 -0
- package/dist/create-app-templates/ecommerce/app/checkout/page.tsx +10 -0
- package/dist/create-app-templates/ecommerce/app/checkout/success/page.tsx +34 -0
- package/dist/create-app-templates/ecommerce/app/favicon.ico +0 -0
- package/dist/create-app-templates/ecommerce/app/globals.css +67 -0
- package/dist/create-app-templates/ecommerce/app/layout.tsx +23 -0
- package/dist/create-app-templates/ecommerce/app/login/page.tsx +11 -0
- package/dist/create-app-templates/ecommerce/app/page.tsx +5 -0
- package/dist/create-app-templates/ecommerce/app/products/[slug]/page.tsx +46 -0
- package/dist/create-app-templates/ecommerce/app/products/page.tsx +45 -0
- package/dist/create-app-templates/ecommerce/app/register/page.tsx +11 -0
- package/dist/create-app-templates/ecommerce/app/webhook/payment/route.ts +20 -0
- package/dist/create-app-templates/ecommerce/app-config.ts +54 -0
- package/dist/create-app-templates/ecommerce/components/auth/auth-form.tsx +109 -0
- package/dist/create-app-templates/ecommerce/components/cart/cart-content.tsx +119 -0
- package/dist/create-app-templates/ecommerce/components/checkout/checkout-form.tsx +267 -0
- package/dist/create-app-templates/ecommerce/components/checkout/checkout-reconcile.tsx +78 -0
- package/dist/create-app-templates/ecommerce/components/layout/account-nav.tsx +48 -0
- package/dist/create-app-templates/ecommerce/components/layout/account-slot.tsx +12 -0
- package/dist/create-app-templates/ecommerce/components/layout/cart-link.tsx +13 -0
- package/dist/create-app-templates/ecommerce/components/layout/page-shell.tsx +11 -0
- package/dist/create-app-templates/ecommerce/components/layout/site-header.tsx +22 -0
- package/dist/create-app-templates/ecommerce/components/product/add-to-cart.tsx +116 -0
- package/dist/create-app-templates/ecommerce/components/product/product-card.tsx +50 -0
- package/dist/create-app-templates/ecommerce/components/product/product-gallery.tsx +39 -0
- package/dist/create-app-templates/ecommerce/data/mock-catalog.json +173 -0
- package/dist/create-app-templates/ecommerce/eslint.config.mjs +18 -0
- package/dist/create-app-templates/ecommerce/lib/cart/cookie.ts +40 -0
- package/dist/create-app-templates/ecommerce/lib/cart/normalize.ts +32 -0
- package/dist/create-app-templates/ecommerce/lib/cart/parse-cart-request.ts +56 -0
- package/dist/create-app-templates/ecommerce/lib/cart/route-helpers.ts +17 -0
- package/dist/create-app-templates/ecommerce/lib/cart/select-provider.ts +44 -0
- package/dist/create-app-templates/ecommerce/lib/cart/server-cart.ts +96 -0
- package/dist/create-app-templates/ecommerce/lib/cart/sync-on-login.server.ts +34 -0
- package/dist/create-app-templates/ecommerce/lib/cart/use-cart.tsx +151 -0
- package/dist/create-app-templates/ecommerce/lib/checkout/checkout-errors.ts +22 -0
- package/dist/create-app-templates/ecommerce/lib/checkout/checkout-provider.ts +28 -0
- package/dist/create-app-templates/ecommerce/lib/checkout/parse-checkout-payload.ts +76 -0
- package/dist/create-app-templates/ecommerce/lib/checkout/start-checkout.ts +63 -0
- package/dist/create-app-templates/ecommerce/lib/checkout/types.ts +3 -0
- package/dist/create-app-templates/ecommerce/lib/commerce/adapters/mock.ts +336 -0
- package/dist/create-app-templates/ecommerce/lib/commerce/adapters/software-mappers.ts +312 -0
- package/dist/create-app-templates/ecommerce/lib/commerce/adapters/software.ts +913 -0
- package/dist/create-app-templates/ecommerce/lib/commerce/product-summary.ts +37 -0
- package/dist/create-app-templates/ecommerce/lib/commerce/provider.server.ts +60 -0
- package/dist/create-app-templates/ecommerce/lib/commerce/provider.ts +96 -0
- package/dist/create-app-templates/ecommerce/lib/commerce/stock.ts +37 -0
- package/dist/create-app-templates/ecommerce/lib/commerce/types.ts +206 -0
- package/dist/create-app-templates/ecommerce/lib/commerce/variant-selection.ts +23 -0
- package/dist/create-app-templates/ecommerce/lib/customer/auth-actions.ts +131 -0
- package/dist/create-app-templates/ecommerce/lib/customer/cart-sync.ts +44 -0
- package/dist/create-app-templates/ecommerce/lib/customer/client.server.ts +109 -0
- package/dist/create-app-templates/ecommerce/lib/customer/current-customer.ts +15 -0
- package/dist/create-app-templates/ecommerce/lib/customer/route-guard.ts +58 -0
- package/dist/create-app-templates/ecommerce/lib/customer/route-helpers.ts +75 -0
- package/dist/create-app-templates/ecommerce/lib/customer/session.ts +108 -0
- package/dist/create-app-templates/ecommerce/lib/format.ts +7 -0
- package/dist/create-app-templates/ecommerce/lib/payment/adapters/mock.ts +84 -0
- package/dist/create-app-templates/ecommerce/lib/payment/adapters/portone.ts +254 -0
- package/dist/create-app-templates/ecommerce/lib/payment/adapters/tosspayments.ts +287 -0
- package/dist/create-app-templates/ecommerce/lib/payment/amount-gate.ts +86 -0
- package/dist/create-app-templates/ecommerce/lib/payment/provider.server.ts +51 -0
- package/dist/create-app-templates/ecommerce/lib/payment/provider.ts +18 -0
- package/dist/create-app-templates/ecommerce/lib/payment/sync-order-payment.ts +96 -0
- package/dist/create-app-templates/ecommerce/lib/payment/types.ts +71 -0
- package/dist/create-app-templates/ecommerce/lib/server-only-guard.ts +20 -0
- package/dist/create-app-templates/ecommerce/next-env.d.ts +6 -0
- package/dist/create-app-templates/ecommerce/next.config.ts +16 -0
- package/dist/create-app-templates/ecommerce/package.json +33 -0
- package/dist/create-app-templates/ecommerce/postcss.config.mjs +7 -0
- package/dist/create-app-templates/ecommerce/tests/customer-auth.test.ts +263 -0
- package/dist/create-app-templates/ecommerce/tests/customer-cart.test.ts +392 -0
- package/dist/create-app-templates/ecommerce/tests/domain.test.ts +1537 -0
- package/dist/create-app-templates/ecommerce/tsconfig.json +35 -0
- package/dist/create-app-templates/registry.json +66 -0
- package/dist/create-app.d.ts +40 -0
- package/dist/create-app.js +652 -0
- package/dist/create-app.js.map +1 -0
- package/dist/detect-Bjxp9wcS.d.ts +13 -0
- package/dist/file-ops.d.ts +21 -0
- package/dist/file-ops.js +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.js +4 -3
- package/dist/index.js.map +1 -1
- package/dist/init.d.ts +40 -0
- package/dist/init.js +4 -3
- package/dist/templates.d.ts +27 -0
- package/dist/templates.js +1 -1
- package/package.json +31 -15
- package/dist/chunk-ENQSB4OF.js.map +0 -1
- package/dist/chunk-UA7WNT2F.js.map +0 -1
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { notFound } from "next/navigation";
|
|
2
|
+
import { PageShell } from "@/components/layout/page-shell";
|
|
3
|
+
import { AddToCart } from "@/components/product/add-to-cart";
|
|
4
|
+
import { ProductGallery } from "@/components/product/product-gallery";
|
|
5
|
+
import { summarizeProductAvailability } from "@/lib/commerce/product-summary";
|
|
6
|
+
import { getCommerceProvider } from "@/lib/commerce/provider.server";
|
|
7
|
+
|
|
8
|
+
export default async function ProductDetailPage({
|
|
9
|
+
params,
|
|
10
|
+
}: {
|
|
11
|
+
params: Promise<{ slug: string }>;
|
|
12
|
+
}) {
|
|
13
|
+
const { slug } = await params;
|
|
14
|
+
const detail = await getCommerceProvider().getProductBySlug(slug);
|
|
15
|
+
if (!detail) notFound();
|
|
16
|
+
|
|
17
|
+
const summary = summarizeProductAvailability(detail);
|
|
18
|
+
|
|
19
|
+
return (
|
|
20
|
+
<PageShell>
|
|
21
|
+
<article>
|
|
22
|
+
<ProductGallery detail={detail} />
|
|
23
|
+
<header>
|
|
24
|
+
<p>Product detail</p>
|
|
25
|
+
<h1>{detail.product.title}</h1>
|
|
26
|
+
<p>{detail.product.description}</p>
|
|
27
|
+
<dl>
|
|
28
|
+
<Metric label="Price" value={summary.priceLabel} />
|
|
29
|
+
<Metric label="Variants" value={String(detail.variants.length)} />
|
|
30
|
+
<Metric label="Price check" value="Before payment" />
|
|
31
|
+
</dl>
|
|
32
|
+
</header>
|
|
33
|
+
<AddToCart detail={detail} />
|
|
34
|
+
</article>
|
|
35
|
+
</PageShell>
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function Metric({ label, value }: { label: string; value: string }) {
|
|
40
|
+
return (
|
|
41
|
+
<>
|
|
42
|
+
<dt>{label}</dt>
|
|
43
|
+
<dd>{value}</dd>
|
|
44
|
+
</>
|
|
45
|
+
);
|
|
46
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { paymentProviderLabel } from "@/app-config";
|
|
2
|
+
import { PageShell } from "@/components/layout/page-shell";
|
|
3
|
+
import { ProductCard } from "@/components/product/product-card";
|
|
4
|
+
import { getCommerceProvider } from "@/lib/commerce/provider.server";
|
|
5
|
+
|
|
6
|
+
export default async function ProductsPage() {
|
|
7
|
+
const commerce = getCommerceProvider();
|
|
8
|
+
const result = await commerce.listProducts({ limit: 24 });
|
|
9
|
+
|
|
10
|
+
return (
|
|
11
|
+
<PageShell>
|
|
12
|
+
<section>
|
|
13
|
+
<p>Starter catalog</p>
|
|
14
|
+
<h1>Storefront flow for payment adapter checks.</h1>
|
|
15
|
+
<p>
|
|
16
|
+
Browse the catalog, review a cart, and complete a payment handoff
|
|
17
|
+
against a compact checkout flow built for adapter validation.
|
|
18
|
+
</p>
|
|
19
|
+
<dl>
|
|
20
|
+
<dt>Commerce</dt>
|
|
21
|
+
<dd>Mock / SDK</dd>
|
|
22
|
+
<dt>Payment</dt>
|
|
23
|
+
<dd>{paymentProviderLabel()} adapter</dd>
|
|
24
|
+
<dt>Price check</dt>
|
|
25
|
+
<dd>Before payment</dd>
|
|
26
|
+
</dl>
|
|
27
|
+
</section>
|
|
28
|
+
|
|
29
|
+
{result.products.length > 0 ? (
|
|
30
|
+
<ul aria-label="Products">
|
|
31
|
+
{result.products.map((detail, index) => (
|
|
32
|
+
<li key={detail.product.id}>
|
|
33
|
+
<ProductCard detail={detail} priority={index < 3} />
|
|
34
|
+
</li>
|
|
35
|
+
))}
|
|
36
|
+
</ul>
|
|
37
|
+
) : (
|
|
38
|
+
<section>
|
|
39
|
+
<h2>No products published</h2>
|
|
40
|
+
<p>Add products or switch the commerce provider.</p>
|
|
41
|
+
</section>
|
|
42
|
+
)}
|
|
43
|
+
</PageShell>
|
|
44
|
+
);
|
|
45
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { PageShell } from "@/components/layout/page-shell";
|
|
2
|
+
import { AuthForm } from "@/components/auth/auth-form";
|
|
3
|
+
|
|
4
|
+
export default function RegisterPage() {
|
|
5
|
+
return (
|
|
6
|
+
<PageShell>
|
|
7
|
+
<h1>Create account</h1>
|
|
8
|
+
<AuthForm mode="register" />
|
|
9
|
+
</PageShell>
|
|
10
|
+
);
|
|
11
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { getCommerceProvider } from "@/lib/commerce/provider.server";
|
|
2
|
+
import { getPaymentProvider } from "@/lib/payment/provider.server";
|
|
3
|
+
import { syncOrderPayment } from "@/lib/payment/sync-order-payment.ts";
|
|
4
|
+
|
|
5
|
+
export async function POST(request: Request) {
|
|
6
|
+
try {
|
|
7
|
+
const payment = getPaymentProvider();
|
|
8
|
+
const event = await payment.verifyWebhook(request);
|
|
9
|
+
const result = await syncOrderPayment({
|
|
10
|
+
paymentId: event.paymentId,
|
|
11
|
+
providerEventId: event.eventId,
|
|
12
|
+
commerceProvider: getCommerceProvider(),
|
|
13
|
+
paymentProvider: payment,
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
return Response.json(result);
|
|
17
|
+
} catch {
|
|
18
|
+
return Response.json({ code: "invalid_webhook" }, { status: 400 });
|
|
19
|
+
}
|
|
20
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Generated app configuration.
|
|
3
|
+
*
|
|
4
|
+
* `create-01-software-app` regenerates this file from `templates/registry.json`
|
|
5
|
+
* when it scaffolds a project, setting `brand`, `title`, `cartKey`, and the
|
|
6
|
+
* selected `paymentProvider`. Generic source files read their per-app values
|
|
7
|
+
* from here so they stay byte-identical across every scaffolded app — which
|
|
8
|
+
* structurally prevents the per-fork drift that previously let a fix land on
|
|
9
|
+
* only one payment template.
|
|
10
|
+
*
|
|
11
|
+
* The committed defaults below keep this in-repo source building and testing
|
|
12
|
+
* with every payment adapter present.
|
|
13
|
+
*/
|
|
14
|
+
export type PaymentProviderId = "portone" | "tosspayments";
|
|
15
|
+
|
|
16
|
+
export interface AppConfig {
|
|
17
|
+
/** Brand name shown in the storefront chrome. */
|
|
18
|
+
brand: string;
|
|
19
|
+
/** Document title for the app. */
|
|
20
|
+
title: string;
|
|
21
|
+
/**
|
|
22
|
+
* Cookie name holding the HttpOnly guest-cart ownership token (`cartToken`).
|
|
23
|
+
* The cart itself is server-authoritative (`commerce.cart.*`); only this
|
|
24
|
+
* capability handle lives in the browser, and never in client-readable JS.
|
|
25
|
+
*/
|
|
26
|
+
cartKey: string;
|
|
27
|
+
/**
|
|
28
|
+
* Cookie name holding the HttpOnly customer session JWT. The token is the
|
|
29
|
+
* customer credential for `client.customer.auth.*`; it is set/cleared only by
|
|
30
|
+
* the `app/api/auth/*` route handlers and is never readable by client JS.
|
|
31
|
+
*/
|
|
32
|
+
customerKey: string;
|
|
33
|
+
/** Payment provider this app was scaffolded for. */
|
|
34
|
+
paymentProvider: PaymentProviderId;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export const appConfig: AppConfig = {
|
|
38
|
+
brand: "01 Commerce",
|
|
39
|
+
title: "01 Commerce Storefront",
|
|
40
|
+
cartKey: "ecommerce-cart",
|
|
41
|
+
customerKey: "ecommerce-customer",
|
|
42
|
+
paymentProvider: "portone",
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
export const PAYMENT_PROVIDER_LABELS: Record<PaymentProviderId, string> = {
|
|
46
|
+
portone: "PortOne",
|
|
47
|
+
tosspayments: "TossPayments",
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
export function paymentProviderLabel(
|
|
51
|
+
provider: PaymentProviderId = appConfig.paymentProvider,
|
|
52
|
+
): string {
|
|
53
|
+
return PAYMENT_PROVIDER_LABELS[provider];
|
|
54
|
+
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useRouter } from "next/navigation";
|
|
4
|
+
import Link from "next/link";
|
|
5
|
+
import { useState } from "react";
|
|
6
|
+
|
|
7
|
+
type Mode = "login" | "register";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Minimal login / register form. Posts JSON to the CSRF-guarded auth routes
|
|
11
|
+
* (same-origin + `application/json`), then refreshes so the header re-renders
|
|
12
|
+
* with the new session.
|
|
13
|
+
*/
|
|
14
|
+
export function AuthForm({ mode }: { mode: Mode }) {
|
|
15
|
+
const router = useRouter();
|
|
16
|
+
const [error, setError] = useState<string | null>(null);
|
|
17
|
+
const [pending, setPending] = useState(false);
|
|
18
|
+
|
|
19
|
+
async function onSubmit(event: React.FormEvent<HTMLFormElement>) {
|
|
20
|
+
event.preventDefault();
|
|
21
|
+
setError(null);
|
|
22
|
+
setPending(true);
|
|
23
|
+
const form = new FormData(event.currentTarget);
|
|
24
|
+
const body: Record<string, string> = {
|
|
25
|
+
email: String(form.get("email") ?? ""),
|
|
26
|
+
password: String(form.get("password") ?? ""),
|
|
27
|
+
};
|
|
28
|
+
if (mode === "register") {
|
|
29
|
+
body.name = String(form.get("name") ?? "");
|
|
30
|
+
const phone = String(form.get("phone") ?? "").trim();
|
|
31
|
+
if (phone) body.phone = phone;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
try {
|
|
35
|
+
const res = await fetch(`/api/auth/${mode}`, {
|
|
36
|
+
method: "POST",
|
|
37
|
+
headers: { "Content-Type": "application/json" },
|
|
38
|
+
body: JSON.stringify(body),
|
|
39
|
+
});
|
|
40
|
+
if (!res.ok) {
|
|
41
|
+
const data = (await res.json().catch(() => null)) as {
|
|
42
|
+
message?: string;
|
|
43
|
+
} | null;
|
|
44
|
+
setError(data?.message ?? "Something went wrong");
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
const data = (await res.json().catch(() => null)) as {
|
|
48
|
+
requiresLogin?: boolean;
|
|
49
|
+
} | null;
|
|
50
|
+
if (mode === "register" && data?.requiresLogin) {
|
|
51
|
+
router.push("/login");
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
router.push("/products");
|
|
55
|
+
router.refresh();
|
|
56
|
+
} finally {
|
|
57
|
+
setPending(false);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return (
|
|
62
|
+
<form onSubmit={onSubmit}>
|
|
63
|
+
{mode === "register" && (
|
|
64
|
+
<label>
|
|
65
|
+
Name
|
|
66
|
+
<input name="name" type="text" autoComplete="name" required />
|
|
67
|
+
</label>
|
|
68
|
+
)}
|
|
69
|
+
<label>
|
|
70
|
+
Email
|
|
71
|
+
<input name="email" type="email" autoComplete="email" required />
|
|
72
|
+
</label>
|
|
73
|
+
<label>
|
|
74
|
+
Password
|
|
75
|
+
<input
|
|
76
|
+
name="password"
|
|
77
|
+
type="password"
|
|
78
|
+
autoComplete={mode === "login" ? "current-password" : "new-password"}
|
|
79
|
+
required
|
|
80
|
+
/>
|
|
81
|
+
</label>
|
|
82
|
+
{mode === "register" && (
|
|
83
|
+
<label>
|
|
84
|
+
Phone (optional)
|
|
85
|
+
<input name="phone" type="tel" autoComplete="tel" />
|
|
86
|
+
</label>
|
|
87
|
+
)}
|
|
88
|
+
{error && <p role="alert">{error}</p>}
|
|
89
|
+
<button type="submit" disabled={pending}>
|
|
90
|
+
{pending
|
|
91
|
+
? "Please wait…"
|
|
92
|
+
: mode === "login"
|
|
93
|
+
? "Log in"
|
|
94
|
+
: "Create account"}
|
|
95
|
+
</button>
|
|
96
|
+
<p>
|
|
97
|
+
{mode === "login" ? (
|
|
98
|
+
<>
|
|
99
|
+
No account? <Link href="/register">Register</Link>
|
|
100
|
+
</>
|
|
101
|
+
) : (
|
|
102
|
+
<>
|
|
103
|
+
Already have an account? <Link href="/login">Log in</Link>
|
|
104
|
+
</>
|
|
105
|
+
)}
|
|
106
|
+
</p>
|
|
107
|
+
</form>
|
|
108
|
+
);
|
|
109
|
+
}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import Image from "next/image";
|
|
4
|
+
import Link from "next/link";
|
|
5
|
+
import { useCart } from "@/lib/cart/use-cart";
|
|
6
|
+
import { formatMoney } from "@/lib/format";
|
|
7
|
+
|
|
8
|
+
export function CartContent() {
|
|
9
|
+
const { cart, loading, updateItem, removeItem } = useCart();
|
|
10
|
+
|
|
11
|
+
if (loading && !cart) {
|
|
12
|
+
return (
|
|
13
|
+
<section>
|
|
14
|
+
<h1>Loading your cart…</h1>
|
|
15
|
+
</section>
|
|
16
|
+
);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const items = cart?.items ?? [];
|
|
20
|
+
|
|
21
|
+
if (items.length === 0) {
|
|
22
|
+
return (
|
|
23
|
+
<section>
|
|
24
|
+
<h1>Your cart is empty</h1>
|
|
25
|
+
<p>Add products before starting a payment check.</p>
|
|
26
|
+
<Link href="/products">Browse products</Link>
|
|
27
|
+
</section>
|
|
28
|
+
);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
return (
|
|
32
|
+
<div>
|
|
33
|
+
<section aria-labelledby="cart-title">
|
|
34
|
+
<h1 id="cart-title">Items queued for checkout</h1>
|
|
35
|
+
<ul>
|
|
36
|
+
{items.map((item) => (
|
|
37
|
+
<li key={item.cartItemId}>
|
|
38
|
+
<article>
|
|
39
|
+
{item.image ? (
|
|
40
|
+
<Image
|
|
41
|
+
src={item.image.url}
|
|
42
|
+
alt={item.image.alt ?? item.productTitle}
|
|
43
|
+
width={112}
|
|
44
|
+
height={112}
|
|
45
|
+
/>
|
|
46
|
+
) : null}
|
|
47
|
+
<h2>{item.productTitle}</h2>
|
|
48
|
+
<dl>
|
|
49
|
+
<dt>Variant</dt>
|
|
50
|
+
<dd>{item.variantTitle ?? item.variantId}</dd>
|
|
51
|
+
<dt>Line total</dt>
|
|
52
|
+
<dd>{formatMoney(item.lineAmount)}</dd>
|
|
53
|
+
</dl>
|
|
54
|
+
<fieldset>
|
|
55
|
+
<legend>Quantity</legend>
|
|
56
|
+
<button
|
|
57
|
+
type="button"
|
|
58
|
+
aria-label={`Decrease quantity for ${item.productTitle}`}
|
|
59
|
+
disabled={item.quantity <= 1}
|
|
60
|
+
onClick={() =>
|
|
61
|
+
void updateItem({
|
|
62
|
+
cartItemId: item.cartItemId,
|
|
63
|
+
quantity: item.quantity - 1,
|
|
64
|
+
})
|
|
65
|
+
}
|
|
66
|
+
>
|
|
67
|
+
-
|
|
68
|
+
</button>
|
|
69
|
+
<output aria-label={`Quantity for ${item.productTitle}`}>
|
|
70
|
+
{item.quantity}
|
|
71
|
+
</output>
|
|
72
|
+
<button
|
|
73
|
+
type="button"
|
|
74
|
+
aria-label={`Increase quantity for ${item.productTitle}`}
|
|
75
|
+
disabled={item.quantity >= 99}
|
|
76
|
+
onClick={() =>
|
|
77
|
+
void updateItem({
|
|
78
|
+
cartItemId: item.cartItemId,
|
|
79
|
+
quantity: item.quantity + 1,
|
|
80
|
+
})
|
|
81
|
+
}
|
|
82
|
+
>
|
|
83
|
+
+
|
|
84
|
+
</button>
|
|
85
|
+
</fieldset>
|
|
86
|
+
<button
|
|
87
|
+
type="button"
|
|
88
|
+
onClick={() => void removeItem(item.cartItemId)}
|
|
89
|
+
>
|
|
90
|
+
Remove {item.productTitle}
|
|
91
|
+
</button>
|
|
92
|
+
</article>
|
|
93
|
+
</li>
|
|
94
|
+
))}
|
|
95
|
+
</ul>
|
|
96
|
+
</section>
|
|
97
|
+
|
|
98
|
+
<aside>
|
|
99
|
+
<h2>Checkout summary</h2>
|
|
100
|
+
<dl>
|
|
101
|
+
<dt>Items</dt>
|
|
102
|
+
<dd>{items.reduce((sum, item) => sum + item.quantity, 0)}</dd>
|
|
103
|
+
<dt>Subtotal</dt>
|
|
104
|
+
<dd>
|
|
105
|
+
<output>{formatMoney(cart?.subtotalAmount ?? 0)}</output>
|
|
106
|
+
</dd>
|
|
107
|
+
<dt>Shipping</dt>
|
|
108
|
+
<dd>{formatMoney(cart?.shippingAmount ?? 0)}</dd>
|
|
109
|
+
<dt>Total</dt>
|
|
110
|
+
<dd>
|
|
111
|
+
<output>{formatMoney(cart?.totalAmount ?? 0)}</output>
|
|
112
|
+
</dd>
|
|
113
|
+
</dl>
|
|
114
|
+
<p>Totals are computed by the server cart and confirmed again at checkout.</p>
|
|
115
|
+
<Link href="/checkout">Checkout</Link>
|
|
116
|
+
</aside>
|
|
117
|
+
</div>
|
|
118
|
+
);
|
|
119
|
+
}
|