lib
HextorStore sample data
Opt-in catalogue, cart, orders, customers and discounts covering the states that are easy to get wrong.
npx shadcn@latest add @hextor/store-fixturesSource
import { cartTotals, money } from "@/registry/hextor/lib/store";
import type {
Cart,
CartLine,
Collection,
Customer,
Discount,
Order,
OrderLine,
Product,
ProductImage,
} from "@/registry/hextor/lib/store";
/**
* Sample data for docs previews and tests only — never a `registryDependency`
* of the storefront or dashboard items. A real store's catalogue comes from
* its own commerce backend; shipping this into every install would be dead
* weight nobody asked for, so it stays opt-in (see skill-fixtures.ts for the
* same convention applied to skill-radar).
*
* Prices are THB in satang (minor units): a ฿1,290 hoodie is `129000`.
*/
/**
* A LOCAL placeholder, not a remote one.
*
* `next/image` rejects any host absent from `images.remotePatterns`, so
* fixtures pointing at a placeholder service turn every page into a 500 on
* install until the consumer edits `next.config.ts` — a template that needs
* config surgery before it renders is a template nobody gets to evaluate.
* `@hextor/store-ui` ships `public/placeholder.svg` alongside the components
* for exactly this reason. `seed` is kept in the signature because the alt text
* is what a screen reader and a broken image both fall back to.
*/
function image(seed: string, width: number, height: number, alt: string): ProductImage {
return { url: "/placeholder.svg", alt: alt || seed, width, height };
}
// ─────────────────────────── collections ───────────────────────────
export const collections: Collection[] = [
{
id: "col_basics",
handle: "everyday-basics",
title: "Everyday Basics",
description: "Tees, hoodies and socks built to be worn on repeat.",
image: image("Everyday Basics", 1200, 675, "Folded stack of plain t-shirts in a wardrobe"),
productIds: ["prod_crew_tee", "prod_hoodie", "prod_socks"],
},
{
id: "col_outerwear",
handle: "monsoon-outerwear",
title: "Monsoon Outerwear",
description: "Rain-ready shells for Bangkok's wet season.",
image: image("Monsoon Outerwear", 1200, 675, "Model wearing a rain shell jacket in the street"),
productIds: ["prod_anorak", "prod_field_jacket"],
},
{
id: "col_accessories",
handle: "weekend-accessories",
title: "Weekend Accessories",
description: "Bags and small goods for a two-day trip.",
image: image("Weekend Accessories", 1200, 675, "Canvas tote and scarf laid flat"),
productIds: ["prod_scarf", "prod_tote", "prod_weekender"],
},
];
// ─────────────────────────── products ───────────────────────────
/**
* Multi-option product: Size × Colour, six variants. One variant
* (M / White) is out of stock but `continueSellingWhenOutOfStock` — the
* distinction the whole point of this file's isAvailable cases turns on.
*/
export const crewTee: Product = {
id: "prod_crew_tee",
handle: "essential-crew-tee",
title: "Essential Crew Tee",
description: "A midweight cotton crew tee cut for daily wear. Pre-shrunk, garment-dyed, no logo.",
status: "active",
vendor: "Hextor Basics",
tags: ["tee", "cotton", "unisex"],
images: [image("Essential Crew Tee", 900, 1125, "Essential Crew Tee, front view, folded")],
options: [
{ name: "Size", values: ["S", "M", "L"] },
{ name: "Colour", values: ["Black", "White"] },
],
variants: [
{
id: "var_crew_tee_s_black",
title: "S / Black",
sku: "ECT-S-BLK",
price: money(59000),
selectedOptions: [{ name: "Size", value: "S" }, { name: "Colour", value: "Black" }],
inventoryQuantity: 24,
},
{
id: "var_crew_tee_s_white",
title: "S / White",
sku: "ECT-S-WHT",
price: money(59000),
selectedOptions: [{ name: "Size", value: "S" }, { name: "Colour", value: "White" }],
inventoryQuantity: 18,
},
{
id: "var_crew_tee_m_black",
title: "M / Black",
sku: "ECT-M-BLK",
price: money(59000),
selectedOptions: [{ name: "Size", value: "M" }, { name: "Colour", value: "Black" }],
inventoryQuantity: 30,
},
{
// Sold out but still sellable — the "backorder" case isAvailable exists for.
id: "var_crew_tee_m_white",
title: "M / White",
sku: "ECT-M-WHT",
price: money(59000),
selectedOptions: [{ name: "Size", value: "M" }, { name: "Colour", value: "White" }],
inventoryQuantity: 0,
continueSellingWhenOutOfStock: true,
},
{
id: "var_crew_tee_l_black",
title: "L / Black",
sku: "ECT-L-BLK",
price: money(59000),
selectedOptions: [{ name: "Size", value: "L" }, { name: "Colour", value: "Black" }],
inventoryQuantity: 15,
},
{
id: "var_crew_tee_l_white",
title: "L / White",
sku: "ECT-L-WHT",
price: money(59000),
selectedOptions: [{ name: "Size", value: "L" }, { name: "Colour", value: "White" }],
inventoryQuantity: 9,
},
],
collectionIds: ["col_basics"],
createdAt: "2025-09-12T08:00:00Z",
};
export const hoodie: Product = {
id: "prod_hoodie",
handle: "heavyweight-hoodie",
title: "Heavyweight Hoodie",
description: "450gsm brushed fleece, kangaroo pocket, ribbed cuffs. Runs true to size.",
status: "active",
vendor: "Hextor Basics",
tags: ["hoodie", "fleece", "unisex"],
images: [image("Heavyweight Hoodie", 900, 1125, "Heavyweight Hoodie laid flat")],
options: [{ name: "Size", values: ["S", "M", "L", "XL"] }],
variants: [
{
id: "var_hoodie_s",
title: "S",
sku: "HWH-S",
price: money(129000),
selectedOptions: [{ name: "Size", value: "S" }],
inventoryQuantity: 20,
},
{
id: "var_hoodie_m",
title: "M",
sku: "HWH-M",
price: money(129000),
selectedOptions: [{ name: "Size", value: "M" }],
inventoryQuantity: 25,
},
{
id: "var_hoodie_l",
title: "L",
sku: "HWH-L",
price: money(129000),
selectedOptions: [{ name: "Size", value: "L" }],
inventoryQuantity: 18,
},
{
id: "var_hoodie_xl",
title: "XL",
sku: "HWH-XL",
price: money(129000),
selectedOptions: [{ name: "Size", value: "XL" }],
inventoryQuantity: 7,
},
],
collectionIds: ["col_basics"],
createdAt: "2025-09-12T08:05:00Z",
};
/** Single-variant product: no real options, one SKU. */
export const socks: Product = {
id: "prod_socks",
handle: "merino-wool-socks-3-pack",
title: "Merino Wool Socks (3-Pack)",
description: "Cushioned merino-blend crew socks. Sold as a set of three, one size.",
status: "active",
vendor: "Hextor Basics",
tags: ["socks", "merino", "gift"],
images: [image("Merino Wool Socks", 900, 1125, "Three pairs of grey wool socks")],
options: [],
variants: [
{
id: "var_socks_default",
title: "Default",
sku: "MWS-3PK",
price: money(25000),
selectedOptions: [],
inventoryQuantity: 150,
},
],
collectionIds: ["col_basics"],
createdAt: "2025-10-01T08:00:00Z",
};
/** On sale: every variant carries a compareAtPrice above its current price. */
export const anorak: Product = {
id: "prod_anorak",
handle: "rain-ready-anorak",
title: "Rain-Ready Anorak",
description: "Taped-seam waterproof shell with a stow-away hood. End-of-season sale.",
status: "active",
vendor: "Hextor Outdoor",
tags: ["jacket", "waterproof", "sale"],
images: [image("Rain-Ready Anorak", 900, 1125, "Yellow anorak shell jacket, front view")],
options: [{ name: "Size", values: ["S", "M", "L"] }],
variants: [
{
id: "var_anorak_s",
title: "S",
sku: "RRA-S",
price: money(249000),
compareAtPrice: money(299000),
selectedOptions: [{ name: "Size", value: "S" }],
inventoryQuantity: 12,
},
{
id: "var_anorak_m",
title: "M",
sku: "RRA-M",
price: money(249000),
compareAtPrice: money(299000),
selectedOptions: [{ name: "Size", value: "M" }],
inventoryQuantity: 20,
},
{
id: "var_anorak_l",
title: "L",
sku: "RRA-L",
price: money(249000),
compareAtPrice: money(299000),
selectedOptions: [{ name: "Size", value: "L" }],
inventoryQuantity: 14,
},
],
collectionIds: ["col_outerwear"],
createdAt: "2025-08-20T08:00:00Z",
};
/** Draft: not yet published to the storefront, still browsable in the dashboard. */
export const fieldJacket: Product = {
id: "prod_field_jacket",
handle: "insulated-field-jacket",
title: "Insulated Field Jacket",
description: "Cold-weather field jacket with a quilted liner. Next season's drop — not live yet.",
status: "draft",
vendor: "Hextor Outdoor",
tags: ["jacket", "insulated"],
images: [image("Insulated Field Jacket", 900, 1125, "Navy field jacket with quilted lining")],
options: [
{ name: "Size", values: ["M", "L"] },
{ name: "Colour", values: ["Navy", "Olive"] },
],
variants: [
{
id: "var_field_jacket_m_navy",
title: "M / Navy",
sku: "IFJ-M-NVY",
price: money(359000),
selectedOptions: [{ name: "Size", value: "M" }, { name: "Colour", value: "Navy" }],
inventoryQuantity: 10,
},
{
id: "var_field_jacket_m_olive",
title: "M / Olive",
sku: "IFJ-M-OLV",
price: money(359000),
selectedOptions: [{ name: "Size", value: "M" }, { name: "Colour", value: "Olive" }],
inventoryQuantity: 10,
},
{
id: "var_field_jacket_l_navy",
title: "L / Navy",
sku: "IFJ-L-NVY",
price: money(359000),
selectedOptions: [{ name: "Size", value: "L" }, { name: "Colour", value: "Navy" }],
inventoryQuantity: 6,
},
{
id: "var_field_jacket_l_olive",
title: "L / Olive",
sku: "IFJ-L-OLV",
price: money(359000),
selectedOptions: [{ name: "Size", value: "L" }, { name: "Colour", value: "Olive" }],
inventoryQuantity: 6,
},
],
collectionIds: ["col_outerwear"],
createdAt: "2026-06-15T08:00:00Z",
};
/** Single-variant product, on the storefront, nothing remarkable — the control case. */
export const scarf: Product = {
id: "prod_scarf",
handle: "silk-touch-scarf",
title: "Silk-Touch Scarf",
description: "Lightweight woven scarf with a silk-touch finish. One size, hand wash.",
status: "active",
vendor: "Hextor Accessories",
tags: ["scarf", "accessory"],
images: [image("Silk-Touch Scarf", 900, 1125, "Charcoal scarf draped over a hook")],
options: [],
variants: [
{
id: "var_scarf_default",
title: "Default",
sku: "STS-CHAR",
price: money(45000),
selectedOptions: [],
inventoryQuantity: 60,
},
],
collectionIds: ["col_accessories"],
createdAt: "2025-11-03T08:00:00Z",
};
/**
* Out of stock: the single variant has zero inventory and does not allow
* continued selling, so `productAvailable` must be false for this one.
*/
export const tote: Product = {
id: "prod_tote",
handle: "canvas-tote-bag",
title: "Canvas Tote Bag",
description: "12oz canvas tote with an interior pocket. Currently sold out — restock pending.",
status: "active",
vendor: "Hextor Accessories",
tags: ["bag", "canvas"],
images: [image("Canvas Tote Bag", 900, 1125, "Undyed canvas tote bag against a wall")],
options: [],
variants: [
{
id: "var_tote_default",
title: "Default",
sku: "CTB-NAT",
price: money(69000),
selectedOptions: [],
inventoryQuantity: 0,
},
],
collectionIds: ["col_accessories"],
createdAt: "2025-07-01T08:00:00Z",
};
export const weekender: Product = {
id: "prod_weekender",
handle: "structured-weekender-bag",
title: "Structured Weekender Bag",
description: "Vegetable-tanned leather trim over a structured canvas body. Fits an overnight trip.",
status: "active",
vendor: "Hextor Accessories",
tags: ["bag", "leather", "travel"],
images: [image("Structured Weekender Bag", 900, 1125, "Weekender duffel bag on a bench")],
options: [{ name: "Colour", values: ["Black", "Tan", "Olive"] }],
variants: [
{
id: "var_weekender_black",
title: "Black",
sku: "SWB-BLK",
price: money(189000),
selectedOptions: [{ name: "Colour", value: "Black" }],
inventoryQuantity: 10,
},
{
id: "var_weekender_tan",
title: "Tan",
sku: "SWB-TAN",
price: money(189000),
selectedOptions: [{ name: "Colour", value: "Tan" }],
inventoryQuantity: 6,
},
{
// Out of stock at the variant level only — Black and Tan keep the
// product itself available, unlike `tote` above.
id: "var_weekender_olive",
title: "Olive",
sku: "SWB-OLV",
price: money(189000),
selectedOptions: [{ name: "Colour", value: "Olive" }],
inventoryQuantity: 0,
},
],
collectionIds: ["col_accessories"],
createdAt: "2025-12-05T08:00:00Z",
};
export const products: Product[] = [
crewTee,
hoodie,
socks,
anorak,
fieldJacket,
scarf,
tote,
weekender,
];
// ─────────────────────────── discounts ───────────────────────────
export const discountWelcome: Discount = {
id: "disc_welcome10",
code: "WELCOME10",
kind: "percentage",
value: 10,
usageCount: 214,
startsAt: "2025-01-01T00:00:00Z",
active: true,
};
export const discountFreeShipping: Discount = {
id: "disc_freeship",
code: "FREESHIP",
kind: "free_shipping",
value: 0,
minimumSubtotal: money(150000),
usageCount: 58,
startsAt: "2025-06-01T00:00:00Z",
active: true,
};
/** Expired and deactivated — exists to prove an inactive code is ignored entirely. */
export const discountSummerSale: Discount = {
id: "disc_summer50",
code: "SUMMER50",
kind: "fixed_amount",
value: 50000,
usageLimit: 500,
usageCount: 500,
startsAt: "2025-05-01T00:00:00Z",
endsAt: "2025-06-30T23:59:59Z",
active: false,
};
export const discounts: Discount[] = [discountWelcome, discountFreeShipping, discountSummerSale];
// ─────────────────────────── cart ───────────────────────────
export const cartLines: CartLine[] = [
{
id: "line_1",
productId: crewTee.id,
variantId: "var_crew_tee_m_black",
title: crewTee.title,
variantTitle: "M / Black",
image: crewTee.images[0],
unitPrice: money(59000),
quantity: 2,
},
{
id: "line_2",
productId: anorak.id,
variantId: "var_anorak_m",
title: anorak.title,
variantTitle: "M",
image: anorak.images[0],
unitPrice: money(249000),
quantity: 1,
},
{
id: "line_3",
productId: weekender.id,
variantId: "var_weekender_tan",
title: weekender.title,
variantTitle: "Tan",
image: weekender.images[0],
unitPrice: money(189000),
quantity: 1,
},
];
export const cart: Cart = {
id: "cart_1",
lines: cartLines,
currency: "THB",
discountCode: "WELCOME10",
};
// ─────────────────────────── orders ───────────────────────────
const orderShippingAddress = {
name: "Napat Suwanwong",
line1: "88/12 Sukhumvit Soi 24",
city: "Bangkok",
province: "Bangkok",
postalCode: "10110",
country: "TH",
phone: "+66812345678",
};
function orderLine(
overrides: Partial<OrderLine> & Pick<OrderLine, "id" | "productId" | "variantId" | "title" | "variantTitle" | "unitPrice" | "quantity">
): OrderLine {
return { fulfilledQuantity: 0, ...overrides };
}
// paid + unfulfilled: a normal just-placed order, payment captured, nothing shipped yet.
const order1Lines: OrderLine[] = [
orderLine({
id: "oline_1001_1",
productId: hoodie.id,
variantId: "var_hoodie_l",
title: hoodie.title,
variantTitle: "L",
unitPrice: money(129000),
quantity: 1,
}),
orderLine({
id: "oline_1001_2",
productId: socks.id,
variantId: "var_socks_default",
title: socks.title,
variantTitle: "Default",
unitPrice: money(25000),
quantity: 2,
}),
];
// paid + fulfilled: shipped in full, nothing outstanding.
const order2Lines: OrderLine[] = [
orderLine({
id: "oline_1002_1",
productId: crewTee.id,
variantId: "var_crew_tee_s_black",
title: crewTee.title,
variantTitle: "S / Black",
unitPrice: money(59000),
quantity: 3,
fulfilledQuantity: 3,
}),
orderLine({
id: "oline_1002_2",
productId: scarf.id,
variantId: "var_scarf_default",
title: scarf.title,
variantTitle: "Default",
unitPrice: money(45000),
quantity: 1,
fulfilledQuantity: 1,
}),
];
// pending + unfulfilled: payment not yet captured (e.g. bank transfer awaited).
const order3Lines: OrderLine[] = [
orderLine({
id: "oline_1003_1",
productId: anorak.id,
variantId: "var_anorak_m",
title: anorak.title,
variantTitle: "M",
unitPrice: money(249000),
quantity: 1,
}),
];
// refunded + fulfilled: shipped, then refunded in full after a return request.
const order4Lines: OrderLine[] = [
orderLine({
id: "oline_1004_1",
productId: weekender.id,
variantId: "var_weekender_black",
title: weekender.title,
variantTitle: "Black",
unitPrice: money(189000),
quantity: 1,
fulfilledQuantity: 1,
}),
];
// paid + partially_fulfilled: one line shipped in full, the other only half —
// the case that proves payment and fulfilment are independent axes.
const order5Lines: OrderLine[] = [
orderLine({
id: "oline_1005_1",
productId: hoodie.id,
variantId: "var_hoodie_m",
title: hoodie.title,
variantTitle: "M",
unitPrice: money(129000),
quantity: 2,
fulfilledQuantity: 1,
}),
orderLine({
id: "oline_1005_2",
productId: socks.id,
variantId: "var_socks_default",
title: socks.title,
variantTitle: "Default",
unitPrice: money(25000),
quantity: 1,
fulfilledQuantity: 1,
}),
];
// voided + cancelled: the customer cancelled before the authorization was captured.
const order6Lines: OrderLine[] = [
orderLine({
id: "oline_1006_1",
productId: anorak.id,
variantId: "var_anorak_s",
title: anorak.title,
variantTitle: "S",
unitPrice: money(249000),
quantity: 1,
}),
];
export const orders: Order[] = [
{
id: "order_1001",
number: "#1001",
customerId: "cust_napat",
email: "napat.s@example.com",
lines: order1Lines,
totals: cartTotals(order1Lines, "THB", { shipping: money(5000), tax: money(12530) }),
paymentStatus: "paid",
fulfillmentStatus: "unfulfilled",
shippingAddress: orderShippingAddress,
billingAddress: orderShippingAddress,
createdAt: "2026-07-28T03:12:00Z",
},
{
id: "order_1002",
number: "#1002",
customerId: "cust_kanokwan",
email: "kanokwan.p@example.com",
lines: order2Lines,
totals: cartTotals(order2Lines, "THB", { shipping: money(0), tax: money(13020) }),
paymentStatus: "paid",
fulfillmentStatus: "fulfilled",
shippingAddress: {
name: "Kanokwan Phetchara",
line1: "212 Thonglor Soi 13",
city: "Bangkok",
province: "Bangkok",
postalCode: "10110",
country: "TH",
},
createdAt: "2026-07-15T10:40:00Z",
},
{
id: "order_1003",
number: "#1003",
email: "guest.checkout@example.com",
lines: order3Lines,
totals: cartTotals(order3Lines, "THB", { shipping: money(6000), tax: money(17430) }),
paymentStatus: "pending",
fulfillmentStatus: "unfulfilled",
note: "Awaiting bank transfer confirmation.",
createdAt: "2026-08-05T14:02:00Z",
},
{
id: "order_1004",
number: "#1004",
customerId: "cust_ananya",
email: "ananya.r@example.com",
lines: order4Lines,
totals: cartTotals(order4Lines, "THB", { discount: discountWelcome, shipping: money(0), tax: money(11340) }),
paymentStatus: "refunded",
fulfillmentStatus: "fulfilled",
note: "Customer returned item — wrong size. Refunded in full.",
shippingAddress: {
name: "Ananya Ruangrit",
line1: "45 Ari Soi 4",
city: "Bangkok",
province: "Bangkok",
postalCode: "10400",
country: "TH",
},
createdAt: "2026-06-02T09:20:00Z",
},
{
id: "order_1005",
number: "#1005",
customerId: "cust_napat",
email: "napat.s@example.com",
lines: order5Lines,
totals: cartTotals(order5Lines, "THB", { shipping: money(5000), tax: money(11480) }),
paymentStatus: "paid",
fulfillmentStatus: "partially_fulfilled",
shippingAddress: orderShippingAddress,
billingAddress: orderShippingAddress,
note: "Hoodie size M restocking — one unit ships when it arrives.",
createdAt: "2026-08-01T06:55:00Z",
},
{
id: "order_1006",
number: "#1006",
customerId: "cust_peerapat",
email: "peerapat.c@example.com",
lines: order6Lines,
totals: cartTotals(order6Lines, "THB", { shipping: money(6000), tax: money(17430) }),
paymentStatus: "voided",
fulfillmentStatus: "cancelled",
note: "Cancelled by customer before authorization was captured.",
createdAt: "2026-05-11T16:00:00Z",
},
];
// ─────────────────────────── customers ───────────────────────────
export const customers: Customer[] = [
{
id: "cust_napat",
name: "Napat Suwanwong",
email: "napat.s@example.com",
phone: "+66812345678",
acceptsMarketing: true,
ordersCount: 2,
totalSpent: money(496010), // sum of order_1001.totals.total + order_1005.totals.total
defaultAddress: orderShippingAddress,
createdAt: "2025-10-02T08:00:00Z",
},
{
id: "cust_kanokwan",
name: "Kanokwan Phetchara",
email: "kanokwan.p@example.com",
acceptsMarketing: true,
ordersCount: 1,
totalSpent: money(235020), // == order_1002.totals.total
defaultAddress: {
name: "Kanokwan Phetchara",
line1: "212 Thonglor Soi 13",
city: "Bangkok",
province: "Bangkok",
postalCode: "10110",
country: "TH",
},
createdAt: "2025-11-20T08:00:00Z",
},
{
// Zero orders: a newsletter signup or an abandoned-checkout lead.
id: "cust_somchai",
name: "Somchai Boonmee",
email: "somchai.b@example.com",
acceptsMarketing: false,
ordersCount: 0,
totalSpent: money(0),
createdAt: "2026-07-30T08:00:00Z",
},
{
id: "cust_ananya",
name: "Ananya Ruangrit",
email: "ananya.r@example.com",
phone: "+66898765432",
acceptsMarketing: false,
ordersCount: 1,
totalSpent: money(0), // order_1004 was refunded in full — net spend is zero
defaultAddress: {
name: "Ananya Ruangrit",
line1: "45 Ari Soi 4",
city: "Bangkok",
province: "Bangkok",
postalCode: "10400",
country: "TH",
},
createdAt: "2025-05-14T08:00:00Z",
},
{
id: "cust_peerapat",
name: "Peerapat Chaiyasit",
email: "peerapat.c@example.com",
phone: "+66865554433",
acceptsMarketing: true,
ordersCount: 1,
totalSpent: money(0), // order_1006 was voided — payment never captured
defaultAddress: {
name: "Peerapat Chaiyasit",
line1: "9 Ratchadamri Road",
line2: "Unit 14B",
city: "Bangkok",
province: "Bangkok",
postalCode: "10330",
country: "TH",
},
createdAt: "2025-08-08T08:00:00Z",
},
];
Docs
Deliberately a separate item and never a registryDependency of a page: sample data should arrive because you asked for it, not because you installed a cart.
The fixtures pick awkward states on purpose — a backorderable variant that is out of stock, a fully sold-out product, a draft, an order that is paid but only partially fulfilled, a customer with zero orders, an expired code. A catalogue where everything is in stock and every order is complete hides exactly the rendering paths worth reviewing.
Prices are in satang: a ฿1,290 hoodie is 129000.
Dependencies
@hextor/store-model