create-cogitato 0.0.0-stage → 0.1.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/LICENSE +202 -0
- package/README.md +12 -2
- package/bin/create-cogitato.js +4 -0
- package/dist/create.d.ts +26 -0
- package/dist/create.js +90 -0
- package/dist/create.js.map +1 -0
- package/package.json +38 -3
- package/template/.claude/settings.json +16 -0
- package/template/.env.example +2 -0
- package/template/CLAUDE.md +34 -0
- package/template/README.md +19 -0
- package/template/_gitignore +3 -0
- package/template/design/contract/components/banner.json +22 -0
- package/template/design/contract/components/button.json +27 -0
- package/template/design/contract/components/card.json +18 -0
- package/template/design/contract/components/input.json +27 -0
- package/template/design/contract/components/stack.json +16 -0
- package/template/design/contract/components/text.json +19 -0
- package/template/design/contract/contract.json +12 -0
- package/template/design/contract/decisions.json +41 -0
- package/template/design/contract/gaps.json +16 -0
- package/template/design/contract/rules.md +19 -0
- package/template/design/contract/tokens.json +126 -0
- package/template/design/flows.json +121 -0
- package/template/index.html +12 -0
- package/template/netlify.toml +28 -0
- package/template/package.json +30 -0
- package/template/pnpm-workspace.yaml +2 -0
- package/template/src/components/Page.module.css +5 -0
- package/template/src/components/Page.tsx +30 -0
- package/template/src/flows.ts +18 -0
- package/template/src/kit/Banner.module.css +48 -0
- package/template/src/kit/Banner.tsx +40 -0
- package/template/src/kit/Button.module.css +86 -0
- package/template/src/kit/Button.tsx +43 -0
- package/template/src/kit/Card.module.css +22 -0
- package/template/src/kit/Card.tsx +18 -0
- package/template/src/kit/Input.module.css +43 -0
- package/template/src/kit/Input.tsx +52 -0
- package/template/src/kit/Stack.tsx +30 -0
- package/template/src/kit/Text.module.css +32 -0
- package/template/src/kit/Text.tsx +19 -0
- package/template/src/main.tsx +11 -0
- package/template/src/mocks/cart.ts +43 -0
- package/template/src/screens/cart/screen.tsx +67 -0
- package/template/src/screens/checkout/confirmation/screen.tsx +34 -0
- package/template/src/screens/checkout/payment/screen.tsx +77 -0
- package/template/src/screens/checkout/shipping/screen.tsx +60 -0
- package/template/src/styles/global.css +12 -0
- package/template/tsconfig.json +17 -0
- package/template/vite.config.ts +7 -0
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
.card {
|
|
2
|
+
background: var(--kit-color-surface-default);
|
|
3
|
+
border: 1px solid var(--kit-color-border-default);
|
|
4
|
+
border-radius: var(--kit-radius-lg);
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
.sm {
|
|
8
|
+
padding: var(--kit-space-3);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
.md {
|
|
12
|
+
padding: var(--kit-space-4);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
.lg {
|
|
16
|
+
padding: var(--kit-space-6);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
.raised {
|
|
20
|
+
border-color: transparent;
|
|
21
|
+
box-shadow: var(--kit-shadow-card);
|
|
22
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { HTMLAttributes, ReactNode } from "react";
|
|
2
|
+
import styles from "./Card.module.css";
|
|
3
|
+
|
|
4
|
+
export interface CardProps extends HTMLAttributes<HTMLDivElement> {
|
|
5
|
+
padding?: "sm" | "md" | "lg";
|
|
6
|
+
elevation?: "flat" | "raised";
|
|
7
|
+
children: ReactNode;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function Card({ padding = "md", elevation = "flat", children, className, ...rest }: CardProps) {
|
|
11
|
+
return (
|
|
12
|
+
<div {...rest} className={[styles.card, styles[padding], styles[elevation], className].filter(Boolean).join(" ")}>
|
|
13
|
+
{children}
|
|
14
|
+
</div>
|
|
15
|
+
);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export default Card;
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
.field {
|
|
2
|
+
display: flex;
|
|
3
|
+
flex-direction: column;
|
|
4
|
+
gap: var(--kit-space-1);
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
.label {
|
|
8
|
+
font: var(--kit-typography-label);
|
|
9
|
+
color: var(--kit-color-text-default);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
.input {
|
|
13
|
+
font: var(--kit-typography-body);
|
|
14
|
+
padding: var(--kit-space-2) var(--kit-space-3);
|
|
15
|
+
border: 1px solid var(--kit-color-border-default);
|
|
16
|
+
border-radius: var(--kit-radius-md);
|
|
17
|
+
background: var(--kit-color-surface-default);
|
|
18
|
+
color: var(--kit-color-text-default);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
.input:focus {
|
|
22
|
+
outline: 2px solid var(--kit-color-border-focus);
|
|
23
|
+
outline-offset: -1px;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
.input:disabled {
|
|
27
|
+
background: var(--kit-color-neutral-100);
|
|
28
|
+
color: var(--kit-color-text-muted);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
.invalid {
|
|
32
|
+
border-color: var(--kit-color-feedback-danger);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
.hint {
|
|
36
|
+
font-size: var(--kit-font-size-sm);
|
|
37
|
+
color: var(--kit-color-text-muted);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
.error {
|
|
41
|
+
font-size: var(--kit-font-size-sm);
|
|
42
|
+
color: var(--kit-color-text-danger);
|
|
43
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { useId, type InputHTMLAttributes } from "react";
|
|
2
|
+
import styles from "./Input.module.css";
|
|
3
|
+
|
|
4
|
+
export interface InputProps extends Omit<InputHTMLAttributes<HTMLInputElement>, "onChange" | "value" | "type"> {
|
|
5
|
+
label: string;
|
|
6
|
+
type?: "text" | "email" | "password" | "number" | "tel";
|
|
7
|
+
/** Starting value. The field stays editable, so prototypes can be typed into. */
|
|
8
|
+
value?: string;
|
|
9
|
+
placeholder?: string;
|
|
10
|
+
/** Help text shown under the field. */
|
|
11
|
+
hint?: string;
|
|
12
|
+
/** Error text. Setting it shows the error state. */
|
|
13
|
+
error?: string;
|
|
14
|
+
disabled?: boolean;
|
|
15
|
+
onChange?: (value: string) => void;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function Input({ label, type = "text", value, hint, error, disabled = false, onChange, className, ...rest }: InputProps) {
|
|
19
|
+
const id = useId();
|
|
20
|
+
const describedBy = error ? `${id}-error` : hint ? `${id}-hint` : undefined;
|
|
21
|
+
return (
|
|
22
|
+
<div className={[styles.field, className].filter(Boolean).join(" ")}>
|
|
23
|
+
<label htmlFor={id} className={styles.label}>
|
|
24
|
+
{label}
|
|
25
|
+
</label>
|
|
26
|
+
<input
|
|
27
|
+
{...rest}
|
|
28
|
+
id={id}
|
|
29
|
+
type={type}
|
|
30
|
+
defaultValue={value}
|
|
31
|
+
disabled={disabled}
|
|
32
|
+
aria-invalid={error ? true : undefined}
|
|
33
|
+
aria-describedby={describedBy}
|
|
34
|
+
className={[styles.input, error && styles.invalid].filter(Boolean).join(" ")}
|
|
35
|
+
onChange={(e) => onChange?.(e.target.value)}
|
|
36
|
+
/>
|
|
37
|
+
{error ? (
|
|
38
|
+
<span id={`${id}-error`} className={styles.error}>
|
|
39
|
+
{error}
|
|
40
|
+
</span>
|
|
41
|
+
) : (
|
|
42
|
+
hint && (
|
|
43
|
+
<span id={`${id}-hint`} className={styles.hint}>
|
|
44
|
+
{hint}
|
|
45
|
+
</span>
|
|
46
|
+
)
|
|
47
|
+
)}
|
|
48
|
+
</div>
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export default Input;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { HTMLAttributes, ReactNode } from "react";
|
|
2
|
+
|
|
3
|
+
export interface StackProps extends HTMLAttributes<HTMLDivElement> {
|
|
4
|
+
direction?: "row" | "column";
|
|
5
|
+
/** Step on the space scale. */
|
|
6
|
+
gap?: "0" | "1" | "2" | "3" | "4" | "6" | "8";
|
|
7
|
+
align?: "start" | "center" | "end" | "stretch";
|
|
8
|
+
children: ReactNode;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const ALIGN = { start: "flex-start", center: "center", end: "flex-end", stretch: "stretch" } as const;
|
|
12
|
+
|
|
13
|
+
export function Stack({ direction = "column", gap = "4", align = "stretch", children, style, ...rest }: StackProps) {
|
|
14
|
+
return (
|
|
15
|
+
<div
|
|
16
|
+
{...rest}
|
|
17
|
+
style={{
|
|
18
|
+
display: "flex",
|
|
19
|
+
flexDirection: direction,
|
|
20
|
+
alignItems: ALIGN[align],
|
|
21
|
+
gap: `var(--kit-space-${gap})`,
|
|
22
|
+
...style,
|
|
23
|
+
}}
|
|
24
|
+
>
|
|
25
|
+
{children}
|
|
26
|
+
</div>
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export default Stack;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
.text {
|
|
2
|
+
margin: 0;
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
.body {
|
|
6
|
+
font: var(--kit-typography-body);
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
.label {
|
|
10
|
+
font: var(--kit-typography-label);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
.heading {
|
|
14
|
+
font: var(--kit-typography-heading);
|
|
15
|
+
letter-spacing: var(--kit-typography-heading-letter-spacing);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
.default {
|
|
19
|
+
color: var(--kit-color-text-default);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
.muted {
|
|
23
|
+
color: var(--kit-color-text-muted);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
.danger {
|
|
27
|
+
color: var(--kit-color-text-danger);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
.inverse {
|
|
31
|
+
color: var(--kit-color-text-inverse);
|
|
32
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { HTMLAttributes, ReactNode } from "react";
|
|
2
|
+
import styles from "./Text.module.css";
|
|
3
|
+
|
|
4
|
+
export interface TextProps extends HTMLAttributes<HTMLElement> {
|
|
5
|
+
variant?: "body" | "label" | "heading";
|
|
6
|
+
tone?: "default" | "muted" | "danger" | "inverse";
|
|
7
|
+
as?: "p" | "span" | "h1" | "h2" | "h3" | "label";
|
|
8
|
+
children: ReactNode;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function Text({ variant = "body", tone = "default", as: Tag = "p", children, className, ...rest }: TextProps) {
|
|
12
|
+
return (
|
|
13
|
+
<Tag {...rest} className={[styles.text, styles[variant], styles[tone], className].filter(Boolean).join(" ")}>
|
|
14
|
+
{children}
|
|
15
|
+
</Tag>
|
|
16
|
+
);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export default Text;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import "virtual:kit/tokens.css";
|
|
2
|
+
import "./styles/global.css";
|
|
3
|
+
import kit from "virtual:kit/kit";
|
|
4
|
+
import { mountPrototype, type ScreenDefinition } from "@cogitato/runtime";
|
|
5
|
+
|
|
6
|
+
// Every src/screens/**/screen.tsx registers one screen with defineScreen.
|
|
7
|
+
const screens = Object.values(
|
|
8
|
+
import.meta.glob<ScreenDefinition>("./screens/**/screen.tsx", { eager: true, import: "default" }),
|
|
9
|
+
);
|
|
10
|
+
|
|
11
|
+
mountPrototype(document.getElementById("root"), { screens, kit });
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
// Mock data for the checkout flow. Prototypes have no backend; fixtures pick from these.
|
|
2
|
+
|
|
3
|
+
export interface CartItem {
|
|
4
|
+
id: string;
|
|
5
|
+
name: string;
|
|
6
|
+
variant: string;
|
|
7
|
+
quantity: number;
|
|
8
|
+
price: number;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface Cart {
|
|
12
|
+
items: CartItem[];
|
|
13
|
+
shipping: number;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export const sampleCart: Cart = {
|
|
17
|
+
items: [
|
|
18
|
+
{ id: "tote", name: "Canvas tote", variant: "Natural", quantity: 1, price: 24 },
|
|
19
|
+
{ id: "mug", name: "Ceramic mug", variant: "Speckled white", quantity: 2, price: 18 },
|
|
20
|
+
],
|
|
21
|
+
shipping: 5,
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export const emptyCart: Cart = { items: [], shipping: 0 };
|
|
25
|
+
|
|
26
|
+
export const subtotal = (cart: Cart) => cart.items.reduce((sum, i) => sum + i.price * i.quantity, 0);
|
|
27
|
+
|
|
28
|
+
export const money = (amount: number) =>
|
|
29
|
+
new Intl.NumberFormat("en-US", { style: "currency", currency: "USD" }).format(amount);
|
|
30
|
+
|
|
31
|
+
export interface Address {
|
|
32
|
+
name: string;
|
|
33
|
+
street: string;
|
|
34
|
+
city: string;
|
|
35
|
+
postcode: string;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export const sampleAddress: Address = {
|
|
39
|
+
name: "Sam Rivera",
|
|
40
|
+
street: "12 Harbour Lane",
|
|
41
|
+
city: "Portland",
|
|
42
|
+
postcode: "97201",
|
|
43
|
+
};
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { defineScreen, useFixture, useGoToState } from "@cogitato/runtime";
|
|
2
|
+
import { Page } from "@/components/Page";
|
|
3
|
+
import { Button } from "@/kit/Button";
|
|
4
|
+
import { Card } from "@/kit/Card";
|
|
5
|
+
import { Stack } from "@/kit/Stack";
|
|
6
|
+
import { Text } from "@/kit/Text";
|
|
7
|
+
import { emptyCart, money, sampleCart, subtotal, type Cart } from "@/mocks/cart";
|
|
8
|
+
|
|
9
|
+
function CartScreen() {
|
|
10
|
+
const { cart } = useFixture<{ cart: Cart }>();
|
|
11
|
+
const goTo = useGoToState();
|
|
12
|
+
|
|
13
|
+
if (cart.items.length === 0) {
|
|
14
|
+
return (
|
|
15
|
+
<Page title="Your cart" step="Cart">
|
|
16
|
+
<Card padding="lg">
|
|
17
|
+
<Stack gap="3" align="start">
|
|
18
|
+
<Text>Your cart is empty.</Text>
|
|
19
|
+
<Button variant="secondary" data-kit-id="cart.keep-shopping">
|
|
20
|
+
Keep shopping
|
|
21
|
+
</Button>
|
|
22
|
+
</Stack>
|
|
23
|
+
</Card>
|
|
24
|
+
</Page>
|
|
25
|
+
);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
return (
|
|
29
|
+
<Page title="Your cart" step="Cart">
|
|
30
|
+
<Card>
|
|
31
|
+
<Stack gap="4">
|
|
32
|
+
{cart.items.map((item) => (
|
|
33
|
+
<Stack key={item.id} direction="row" gap="4">
|
|
34
|
+
<Stack gap="1" style={{ flex: 1 }}>
|
|
35
|
+
<Text variant="label">{item.name}</Text>
|
|
36
|
+
<Text tone="muted">
|
|
37
|
+
{item.variant} · Qty {item.quantity}
|
|
38
|
+
</Text>
|
|
39
|
+
</Stack>
|
|
40
|
+
<Text>{money(item.price * item.quantity)}</Text>
|
|
41
|
+
</Stack>
|
|
42
|
+
))}
|
|
43
|
+
</Stack>
|
|
44
|
+
</Card>
|
|
45
|
+
<Stack direction="row" gap="4">
|
|
46
|
+
<Text variant="label" style={{ flex: 1 }}>
|
|
47
|
+
Subtotal
|
|
48
|
+
</Text>
|
|
49
|
+
<Text variant="label">{money(subtotal(cart))}</Text>
|
|
50
|
+
</Stack>
|
|
51
|
+
<Button fullWidth size="lg" data-kit-id="cart.checkout" onPress={() => goTo("checkout.shipping.default")}>
|
|
52
|
+
Check out
|
|
53
|
+
</Button>
|
|
54
|
+
</Page>
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export default defineScreen({
|
|
59
|
+
id: "cart",
|
|
60
|
+
route: "/cart",
|
|
61
|
+
title: "Cart",
|
|
62
|
+
fixtures: {
|
|
63
|
+
default: { title: "Two items", data: { cart: sampleCart } },
|
|
64
|
+
empty: { title: "Empty cart", data: { cart: emptyCart } },
|
|
65
|
+
},
|
|
66
|
+
component: CartScreen,
|
|
67
|
+
});
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { defineScreen, useFixture, useGoToState } from "@cogitato/runtime";
|
|
2
|
+
import { Page } from "@/components/Page";
|
|
3
|
+
import { Button } from "@/kit/Button";
|
|
4
|
+
import { Card } from "@/kit/Card";
|
|
5
|
+
import { Stack } from "@/kit/Stack";
|
|
6
|
+
import { Text } from "@/kit/Text";
|
|
7
|
+
|
|
8
|
+
function ConfirmationScreen() {
|
|
9
|
+
const { orderNumber, email } = useFixture<{ orderNumber: string; email: string }>();
|
|
10
|
+
const goTo = useGoToState();
|
|
11
|
+
return (
|
|
12
|
+
<Page title="Order confirmed">
|
|
13
|
+
<Card padding="lg" elevation="raised">
|
|
14
|
+
<Stack gap="2">
|
|
15
|
+
<Text variant="label">Order {orderNumber}</Text>
|
|
16
|
+
<Text tone="muted">We've sent a receipt to {email}. Your order ships in 1–2 business days.</Text>
|
|
17
|
+
</Stack>
|
|
18
|
+
</Card>
|
|
19
|
+
<Button variant="secondary" data-kit-id="checkout.confirmation.done" onPress={() => goTo("cart.empty")}>
|
|
20
|
+
Back to the shop
|
|
21
|
+
</Button>
|
|
22
|
+
</Page>
|
|
23
|
+
);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export default defineScreen({
|
|
27
|
+
id: "checkout.confirmation",
|
|
28
|
+
route: "/checkout/confirmation",
|
|
29
|
+
title: "Order confirmed",
|
|
30
|
+
fixtures: {
|
|
31
|
+
default: { title: "Confirmed", data: { orderNumber: "#10482", email: "sam@example.com" } },
|
|
32
|
+
},
|
|
33
|
+
component: ConfirmationScreen,
|
|
34
|
+
});
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { useEffect } from "react";
|
|
2
|
+
import { defineScreen, useFixture, useFrameMode, useGoToState } from "@cogitato/runtime";
|
|
3
|
+
import { Page } from "@/components/Page";
|
|
4
|
+
import { Banner } from "@/kit/Banner";
|
|
5
|
+
import { Button } from "@/kit/Button";
|
|
6
|
+
import { Card } from "@/kit/Card";
|
|
7
|
+
import { Input } from "@/kit/Input";
|
|
8
|
+
import { Stack } from "@/kit/Stack";
|
|
9
|
+
import { Text } from "@/kit/Text";
|
|
10
|
+
import { money, sampleCart, subtotal } from "@/mocks/cart";
|
|
11
|
+
|
|
12
|
+
interface PaymentData {
|
|
13
|
+
processing?: boolean;
|
|
14
|
+
declined?: boolean;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const total = subtotal(sampleCart) + sampleCart.shipping;
|
|
18
|
+
|
|
19
|
+
function PaymentScreen() {
|
|
20
|
+
const { processing = false, declined = false } = useFixture<PaymentData>();
|
|
21
|
+
const goTo = useGoToState();
|
|
22
|
+
const frame = useFrameMode();
|
|
23
|
+
|
|
24
|
+
// The mock payment always succeeds after a moment. Canvas frames hold still.
|
|
25
|
+
useEffect(() => {
|
|
26
|
+
if (!processing || frame) return;
|
|
27
|
+
const timer = setTimeout(() => goTo("checkout.confirmation.default"), 1500);
|
|
28
|
+
return () => clearTimeout(timer);
|
|
29
|
+
}, [processing, frame, goTo]);
|
|
30
|
+
|
|
31
|
+
return (
|
|
32
|
+
<Page title="Payment" step="Payment">
|
|
33
|
+
{declined && (
|
|
34
|
+
<Banner tone="danger" title="Your card was declined" data-kit-id="checkout.payment.error-banner">
|
|
35
|
+
Try another card, or check the details with your bank.
|
|
36
|
+
</Banner>
|
|
37
|
+
)}
|
|
38
|
+
<Card>
|
|
39
|
+
<Stack gap="4">
|
|
40
|
+
<Input label="Card number" placeholder="1234 5678 9012 3456" disabled={processing} data-kit-id="checkout.payment.card-number" />
|
|
41
|
+
<Stack direction="row" gap="3">
|
|
42
|
+
<Input label="Expiry" placeholder="MM / YY" disabled={processing} data-kit-id="checkout.payment.expiry" />
|
|
43
|
+
<Input label="Security code" placeholder="123" disabled={processing} data-kit-id="checkout.payment.cvc" />
|
|
44
|
+
</Stack>
|
|
45
|
+
<Input label="Name on card" disabled={processing} data-kit-id="checkout.payment.cardholder" />
|
|
46
|
+
</Stack>
|
|
47
|
+
</Card>
|
|
48
|
+
<Stack direction="row" gap="4">
|
|
49
|
+
<Text variant="label" style={{ flex: 1 }}>
|
|
50
|
+
Total
|
|
51
|
+
</Text>
|
|
52
|
+
<Text variant="label">{money(total)}</Text>
|
|
53
|
+
</Stack>
|
|
54
|
+
<Button
|
|
55
|
+
fullWidth
|
|
56
|
+
size="lg"
|
|
57
|
+
loading={processing}
|
|
58
|
+
data-kit-id="checkout.payment.submit"
|
|
59
|
+
onPress={() => goTo("checkout.payment.processing")}
|
|
60
|
+
>
|
|
61
|
+
{processing ? "Processing" : `Pay ${money(total)}`}
|
|
62
|
+
</Button>
|
|
63
|
+
</Page>
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export default defineScreen<PaymentData>({
|
|
68
|
+
id: "checkout.payment",
|
|
69
|
+
route: "/checkout/payment",
|
|
70
|
+
title: "Payment",
|
|
71
|
+
fixtures: {
|
|
72
|
+
default: { title: "Empty form", data: {} },
|
|
73
|
+
processing: { title: "Processing", data: { processing: true } },
|
|
74
|
+
"card-declined": { title: "Card declined", data: { declined: true } },
|
|
75
|
+
},
|
|
76
|
+
component: PaymentScreen,
|
|
77
|
+
});
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { defineScreen, useFixture, useGoToState } from "@cogitato/runtime";
|
|
2
|
+
import { Page } from "@/components/Page";
|
|
3
|
+
import { Banner } from "@/kit/Banner";
|
|
4
|
+
import { Button } from "@/kit/Button";
|
|
5
|
+
import { Input } from "@/kit/Input";
|
|
6
|
+
import { Stack } from "@/kit/Stack";
|
|
7
|
+
import { sampleAddress, type Address } from "@/mocks/cart";
|
|
8
|
+
|
|
9
|
+
interface ShippingData {
|
|
10
|
+
address: Partial<Address>;
|
|
11
|
+
addressError?: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function ShippingScreen() {
|
|
15
|
+
const { address, addressError } = useFixture<ShippingData>();
|
|
16
|
+
const goTo = useGoToState();
|
|
17
|
+
|
|
18
|
+
return (
|
|
19
|
+
<Page title="Where should we send it?" step="Shipping">
|
|
20
|
+
{addressError && (
|
|
21
|
+
<Banner tone="danger" title="We couldn't find that address" data-kit-id="checkout.shipping.address-banner">
|
|
22
|
+
Check the street and postcode, then try again.
|
|
23
|
+
</Banner>
|
|
24
|
+
)}
|
|
25
|
+
<Stack gap="4">
|
|
26
|
+
<Input label="Full name" value={address.name} data-kit-id="checkout.shipping.name" />
|
|
27
|
+
<Input label="Street address" value={address.street} error={addressError} data-kit-id="checkout.shipping.street" />
|
|
28
|
+
<Stack direction="row" gap="3">
|
|
29
|
+
<Input label="City" value={address.city} data-kit-id="checkout.shipping.city" style={{ flex: 1 }} />
|
|
30
|
+
<Input label="Postcode" value={address.postcode} data-kit-id="checkout.shipping.postcode" />
|
|
31
|
+
</Stack>
|
|
32
|
+
</Stack>
|
|
33
|
+
<Stack gap="2">
|
|
34
|
+
<Button fullWidth size="lg" data-kit-id="checkout.shipping.continue" onPress={() => goTo("checkout.payment.default")}>
|
|
35
|
+
Continue to payment
|
|
36
|
+
</Button>
|
|
37
|
+
<Button variant="ghost" data-kit-id="checkout.shipping.back" onPress={() => goTo("cart.default")}>
|
|
38
|
+
Back to cart
|
|
39
|
+
</Button>
|
|
40
|
+
</Stack>
|
|
41
|
+
</Page>
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export default defineScreen<ShippingData>({
|
|
46
|
+
id: "checkout.shipping",
|
|
47
|
+
route: "/checkout/shipping",
|
|
48
|
+
title: "Shipping",
|
|
49
|
+
fixtures: {
|
|
50
|
+
default: { title: "Empty form", data: { address: {} } },
|
|
51
|
+
"invalid-address": {
|
|
52
|
+
title: "Address not found",
|
|
53
|
+
data: {
|
|
54
|
+
address: { ...sampleAddress, street: "12 Harbor Ln" },
|
|
55
|
+
addressError: "We couldn't match this street to the postcode.",
|
|
56
|
+
},
|
|
57
|
+
},
|
|
58
|
+
},
|
|
59
|
+
component: ShippingScreen,
|
|
60
|
+
});
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"lib": ["ES2022", "DOM", "DOM.Iterable"],
|
|
5
|
+
"module": "ESNext",
|
|
6
|
+
"moduleResolution": "Bundler",
|
|
7
|
+
"jsx": "react-jsx",
|
|
8
|
+
"strict": true,
|
|
9
|
+
"noEmit": true,
|
|
10
|
+
"isolatedModules": true,
|
|
11
|
+
"skipLibCheck": true,
|
|
12
|
+
"resolveJsonModule": true,
|
|
13
|
+
"types": ["vite/client", "@cogitato/runtime/client"],
|
|
14
|
+
"paths": { "@/*": ["./src/*"] }
|
|
15
|
+
},
|
|
16
|
+
"include": ["src", "vite.config.ts"]
|
|
17
|
+
}
|