3 sections
Product features
What sets one product apart: its materials, dimensions and care, beside the picture.
Feature trio with a measured drawing
product-features-01What sets one product apart, in three — what it is made of, how big it is and how to look after it — beside a drawing of the product measured where it counts, with its price, score, sales and the way to its page.
import * as React from "react"
import Link from "next/link"
import { AnvilIcon, ArrowRightIcon, DropletIcon, RulerIcon, type LucideIcon } from "lucide-react"
import { cn } from "@/lib/utils"
import { formatNumber } from "@/lib/format"
import { buttonVariants } from "@/components/ui/button"
import { Card, CardContent } from "@/components/ui/card"
import { Price } from "@/components/ui/price"
import { ProductArt } from "@/components/ui/product-art"
import { Rating } from "@/components/ui/rating"
import { COPY, SHELF_DAYS, featuredProduct, type FeatureIcon } from "./product-features-01.data"
/** The picture each feature is drawn with: vocabulary of the layout, not copy. */
const ICONS: Record<FeatureIcon, LucideIcon> = { material: AnvilIcon, size: RulerIcon, care: DropletIcon }
const score = (value: number) => formatNumber(value, { minimumFractionDigits: 1, maximumFractionDigits: 1 })
/** A point of the drawing's 64-unit grid, as a share of the tile: the drawing is 72% of it, centred. */
const onTile = (unit: number) => `${14 + (unit / 64) * 72}%`
/**
* The measures laid over the drawing: each a hairline between two ticks,
* carried to the edges it measures by dashed lines, with its label above the
* middle. The lines sit on the drawing's own 64-unit grid at the drawing's
* size and place, so a measure meets the rim it measures, and stay a pixel
* at any size; the labels are text at the kit's 12px, placed on the grid.
*/
function Measures({ measures }: { measures: typeof COPY.measures }) {
return (
<div data-slot="measures" aria-hidden="true" className="pointer-events-none absolute inset-0">
<svg viewBox="0 0 64 64" focusable="false" className="absolute inset-0 m-auto aspect-square h-[72%] overflow-visible text-muted-foreground">
{measures.map((measure) => {
const [x1, y1] = measure.from
const [x2, y2] = measure.to
const y = measure.at
return (
<g key={measure.label} stroke="currentColor" fill="none" strokeWidth="1">
<path d={`M${x1} ${y1}V${y}M${x2} ${y2}V${y}`} strokeDasharray="2 2" vectorEffect="non-scaling-stroke" opacity="0.6" />
<path d={`M${x1} ${y}H${x2}M${x1} ${y - 1.2}v2.4M${x2} ${y - 1.2}v2.4`} vectorEffect="non-scaling-stroke" />
</g>
)
})}
</svg>
{measures.map((measure) => (
<span
key={measure.label}
className="absolute -translate-x-1/2 -translate-y-full pb-1 text-xs whitespace-nowrap text-foreground tabular-nums"
style={{ left: onTile((measure.from[0] + measure.to[0]) / 2), top: onTile(measure.at) }}
>
{measure.label}
</span>
))}
</div>
)
}
/**
* What sets one product apart, in three: what it is made of, how big it is
* and how to look after it — each with its icon, beside a drawing of the
* product measured where it counts. Under the three, the product's price, its
* score and what it sold, and the way to its page.
*
* A server component: the product and its figures come from
* `./product-features-01.data`, with the shop's account of it.
*/
export function ProductFeatures01() {
const titleId = React.useId()
const product = featuredProduct()
return (
<section
data-section="product-features-01"
data-slot="section"
aria-labelledby={titleId}
className="w-full bg-background px-4 py-16 text-foreground md:px-6 md:py-24"
>
<div className="mx-auto grid max-w-7xl items-center gap-8 lg:grid-cols-2 lg:gap-16">
<Card className="gap-0 p-2">
<CardContent className="relative p-0">
<ProductArt name={product.name} category={product.category} className="rounded-none" />
<Measures measures={COPY.measures} />
</CardContent>
</Card>
<div className="flex flex-col gap-8">
<div className="flex flex-col gap-2">
<p className="type-eyebrow">{product.name}</p>
<h2 id={titleId} className="type-display text-2xl text-balance md:text-3xl">
{COPY.heading}
</h2>
<p className="text-sm text-pretty text-muted-foreground md:text-base">{COPY.lede}</p>
</div>
<ul className="flex flex-col divide-y divide-border border-y border-border">
{COPY.features.map((feature) => {
const Icon = ICONS[feature.icon]
return (
<li key={feature.title} className="flex gap-4 py-5">
<span aria-hidden="true" className="grid size-9 shrink-0 place-items-center rounded-lg bg-secondary">
<Icon className="size-4 text-foreground" />
</span>
<div className="flex flex-col gap-1">
<h3 className="text-sm font-semibold text-foreground">{feature.title}</h3>
<p className="text-sm text-pretty text-muted-foreground">{feature.body}</p>
</div>
</li>
)
})}
</ul>
<div className="flex flex-wrap items-center justify-between gap-x-6 gap-y-4">
<div className="flex flex-col gap-1.5">
<Price amount={product.priceCents / 100} size="lg" />
<div className="flex flex-wrap items-center gap-x-1.5 gap-y-1 text-xs text-muted-foreground tabular-nums">
{product.ratings ? (
<>
<Rating value={Math.round(product.rating)} size="sm" aria-hidden="true" />
<span aria-hidden="true">
{score(product.rating)} from {product.ratings} ratings ·
</span>
<span className="sr-only">Rated {score(product.rating)} out of 5 from {product.ratings} ratings.</span>{" "}
</>
) : null}
<span>
{formatNumber(product.sold)} sold in the last {SHELF_DAYS} days
</span>
</div>
</div>
<Link href={product.href} className={cn(buttonVariants({ variant: "outline" }), "group/all")}>
{COPY.cta}
<ArrowRightIcon
aria-hidden="true"
data-icon="inline-end"
className="transition-transform duration-(--duration-fast) ease-(--ease-standard) group-hover/all:translate-x-0.5"
/>
</Link>
</div>
</div>
</div>
</section>
)
}
export default ProductFeatures01/**
* What `product-features-01` reads: one product off the storefront's shelf —
* its price, what it sold and what buyers rated it — and the shop's account
* of what sets it apart: what it is made of, how big it is and how to look
* after it. The account is copy, kept here beside the name it describes, and
* so are the two measures drawn over the product's picture, which are placed
* on the drawing's own grid.
*
* Selectors only, reading the store inside functions and run on the server.
*/
import { SHELF_DAYS, storeShelf, type ShelfProduct } from "@/lib/dashboards/ecommerce/storefront"
export { SHELF_DAYS }
/** The product the band shows, and what the shop says about it: change them together. */
export const PRODUCT = "Cast Iron Skillet"
export const COPY = {
heading: "Cast iron, seasoned before it ships",
lede: "One pan that goes from the stove to the oven to the table, and outlasts all three.",
features: [
{
icon: "material",
title: "One piece of iron",
body: "No rivets and no coating to wear through. It is seasoned with flaxseed oil at the foundry, so it goes on the stove the day it arrives.",
},
{
icon: "size",
title: "26 cm across, 2.4 kg",
body: "Five centimeters deep, with a long handle and a helper handle opposite, for lifting it full. The weight is what holds the heat.",
},
{
icon: "care",
title: "Wash, dry, oil",
body: "Rinse it in hot water, dry it on a low flame and wipe a drop of oil around the inside. Every egg after that makes it better.",
},
],
cta: "See the skillet",
/**
* Measures drawn over the picture, on its 64-unit grid: from one edge of
* the pan to another, each edge carried by a dashed line to the level `at`,
* where the measure is drawn with its label. The drawing's own geometry
* fixes the points: the rim spans 8 to 44, the handles 4.3 to 60.
*/
measures: [
{ from: [8, 45], to: [44, 45], at: 58.5, label: "26 cm" },
{ from: [4.3, 39.5], to: [60, 34], at: 25, label: "44 cm with the handle" },
],
} as const
export type FeatureIcon = (typeof COPY.features)[number]["icon"]
/** The product as the band shows it. */
export type FeaturedProduct = Pick<ShelfProduct, "id" | "name" | "category" | "priceCents" | "sold" | "rating" | "ratings" | "href">
/** `PRODUCT` off the shelf. */
export function featuredProduct(): FeaturedProduct {
const product = storeShelf().find((row) => row.name === PRODUCT)
if (!product) throw new Error(`product-features-01 shows "${PRODUCT}", which is not on the shelf`)
const { id, name, category, priceCents, sold, rating, ratings, href } = product
return { id, name, category, priceCents, sold, rating, ratings, href }
}$
npx shadcn@latest add @vibra/product-features-01Spec comparison
product-features-02Five seats from three departments measured side by side — size, build, and the store's own figures — with chips that choose the seats compared, a switch that keeps only the rows where they differ, and the seat bought most held up.
import * as React from "react"
import Link from "next/link"
import { ProductArt } from "@/components/ui/product-art"
import { SeatComparison } from "./product-features-02.client"
import { HEADING, RECORD_ROWS, SPEC_ROWS, seats } from "./product-features-02.data"
/**
* Five seats, measured side by side: from the desk chair to the camp chair,
* how high each sits, what it holds, whether it folds or stores, and what the
* store knows of each — price, score, sales and stock. Chips choose the seats
* compared and a switch keeps only the rows where they differ; the seat
* bought most is held up. On a phone the table scrolls past its pinned
* labels, never the page.
*
* A server component: the seats, their spec sheets and their figures come from
* `./product-features-02.data`; each column's head is drawn here.
*/
export function ProductFeatures02() {
const titleId = React.useId()
const { columns, mostBought } = seats()
const heads = columns.map((seat) => ({
id: seat.id,
name: seat.name,
highlighted: seat.id === mostBought,
caption: (
<span className="mt-2 flex flex-col items-center gap-1.5">
<ProductArt name={seat.name} category={seat.category} className="w-14 rounded-md" />
{seat.id === mostBought ? <span className="font-medium text-foreground">Most bought</span> : null}
<Link href={seat.href} className="focus-ring rounded-sm text-brand underline-offset-4 hover:underline">
See it<span className="sr-only">: the {seat.name}</span>
</Link>
</span>
),
}))
const rows = [...SPEC_ROWS, ...RECORD_ROWS].map((row) => ({
id: row.id,
label: row.label,
values: Object.fromEntries(columns.map((seat) => [seat.id, seat.values[row.id]])),
}))
const groups = [
{ label: "Size", rows: SPEC_ROWS.filter((row) => row.group === "Size").map((row) => row.id) },
{ label: "Build", rows: SPEC_ROWS.filter((row) => row.group === "Build").map((row) => row.id) },
{ label: "From our records", rows: RECORD_ROWS.map((row) => row.id) },
]
return (
<section
data-section="product-features-02"
data-slot="section"
aria-labelledby={titleId}
className="w-full bg-background px-4 py-16 text-foreground md:px-6 md:py-24"
>
<div className="mx-auto max-w-7xl">
<div className="flex max-w-2xl flex-col gap-2">
<p className="type-eyebrow">Compare</p>
<h2 id={titleId} className="type-display text-2xl text-balance md:text-3xl">
{HEADING}
</h2>
<p className="text-sm text-pretty text-muted-foreground md:text-base">
From the desk to the campsite: how high each one sits, what it holds and what it takes to put together.
</p>
</div>
<div className="mt-8 md:mt-10">
<SeatComparison seats={heads} rows={rows} groups={groups} />
</div>
</div>
</section>
)
}
export default ProductFeatures02"use client"
import * as React from "react"
import { CheckIcon } from "lucide-react"
import { ComparisonTable } from "@/components/ui/comparison-table"
import { Label } from "@/components/ui/label"
import { Switch } from "@/components/ui/switch"
import { Toggle } from "@/components/ui/toggle"
/** A seat's column: its name, the head the server drew for it, and whether it is the one held up. */
export type SeatHead = { id: string; name: string; caption: React.ReactNode; highlighted: boolean }
/** One row of the table: what it measures and each seat's value, by seat id. */
export type SeatRow = { id: string; label: string; values: Record<string, string | boolean> }
export type SeatGroup = { label: string; rows: string[] }
/**
* The comparison and the two ways to narrow it: which seats are in it — a
* row of toggle chips, at least two always on — and whether to show only the
* rows where the seats in it differ. The table is the kit's ComparisonTable:
* the label column pinned, the seat bought most tinted and marked, and every
* yes and no said in words. An always-mounted status line says what is being
* compared.
*/
export function SeatComparison({ seats, rows, groups }: { seats: SeatHead[]; rows: SeatRow[]; groups: SeatGroup[] }) {
const [chosen, setChosen] = React.useState<ReadonlySet<string>>(() => new Set(seats.map((seat) => seat.id)))
const [onlyDiffer, setOnlyDiffer] = React.useState(false)
const pickerId = React.useId()
const leastId = React.useId()
const columns = seats.filter((seat) => chosen.has(seat.id))
const differs = (row: SeatRow) => new Set(columns.map((seat) => String(row.values[seat.id]))).size > 1
const shown = onlyDiffer ? rows.filter(differs) : rows
const shownIds = new Set(shown.map((row) => row.id))
const locked = chosen.size <= 2
function flip(id: string) {
const next = new Set(chosen)
if (!next.delete(id)) next.add(id)
if (next.size >= 2) setChosen(next)
}
return (
<div className="flex flex-col gap-4">
<div className="flex flex-col gap-3 lg:flex-row lg:items-center lg:justify-between">
<div role="group" aria-labelledby={pickerId} className="flex flex-wrap items-center gap-1.5">
<span id={pickerId} className="mr-1 text-xs text-muted-foreground">
Compare
</span>
{seats.map((seat) => {
const on = chosen.has(seat.id)
return (
<Toggle
key={seat.id}
size="sm"
pressed={on}
onPressedChange={() => flip(seat.id)}
aria-disabled={(on && locked) || undefined}
aria-describedby={on && locked ? leastId : undefined}
className="rounded-full bg-card px-3 font-normal ring-1 ring-border aria-pressed:bg-brand-muted aria-pressed:font-medium aria-pressed:ring-transparent"
>
{on ? <CheckIcon data-icon="inline-start" /> : null}
{seat.name}
</Toggle>
)
})}
{locked ? (
<span id={leastId} className="ml-1 text-xs text-muted-foreground">
Two at least
</span>
) : null}
</div>
<Label className="w-fit gap-2 text-sm font-normal text-foreground">
<Switch checked={onlyDiffer} onCheckedChange={setOnlyDiffer} />
Only what differs
</Label>
</div>
<p role="status" aria-live="polite" className="text-sm text-muted-foreground tabular-nums">
{onlyDiffer
? `${columns.length} seats, on the ${shown.length} of ${rows.length} rows where they differ`
: `${columns.length} seats, on all ${rows.length} rows`}
</p>
<ComparisonTable
columns={columns.map((seat) => ({ id: seat.id, label: seat.name, caption: seat.caption, highlighted: seat.highlighted }))}
rows={shown.map((row) => ({
id: row.id,
label: row.label,
values: Object.fromEntries(columns.map((seat) => [seat.id, row.values[seat.id]])),
}))}
// A group every row of which is hidden goes too, rather than leave a heading over nothing.
groups={groups
.map((group) => ({ label: group.label, rows: group.rows.filter((id) => shownIds.has(id)) }))
.filter((group) => group.rows.length > 0)}
/>
</div>
)
}/**
* What `product-features-02` reads: five seats from three departments, set
* side by side. What each one is — how high it sits, what it holds, whether
* it folds — is the shop's spec sheet, copy kept here beside the names it
* describes; what the store knows of each — its price, its score, what it
* sold and its stock — is read off the storefront's shelf. The column held
* up is the seat bought most.
*
* Selectors only, reading the store inside functions and run on the server.
* The island takes plain props; it never imports this module.
*/
import { SHELF_DAYS, storeShelf, type ShelfProduct } from "@/lib/dashboards/ecommerce/storefront"
import { formatCurrency, formatNumber } from "@/lib/format"
type SeatSpec = {
seatHeight: string
seatWidth: string
weight: string
holds: string
adjusts: boolean
folds: boolean
storage: boolean
cover: string
assembly: string
}
/** What the band calls the comparison: change it with the seats. */
export const HEADING = "Five seats, measured side by side"
/** The seats compared, and the shop's spec sheet for each: change them together. */
export const SEATS: readonly { name: string; spec: SeatSpec }[] = [
{
name: "Ergonomic Office Chair",
spec: { seatHeight: "44 to 54 cm", seatWidth: "50 cm", weight: "18 kg", holds: "136 kg", adjusts: true, folds: false, storage: false, cover: "Mesh back, foam seat", assembly: "15 minutes" },
},
{
name: "Accent Armchair",
spec: { seatHeight: "43 cm", seatWidth: "58 cm", weight: "16 kg", holds: "120 kg", adjusts: false, folds: false, storage: false, cover: "Wool blend", assembly: "None" },
},
{
name: "Bar Stool Set",
spec: { seatHeight: "65 cm", seatWidth: "36 cm", weight: "7 kg each", holds: "110 kg", adjusts: false, folds: false, storage: false, cover: "Solid oak seat", assembly: "20 minutes" },
},
{
name: "Storage Ottoman",
spec: { seatHeight: "42 cm", seatWidth: "76 cm", weight: "9 kg", holds: "150 kg", adjusts: false, folds: false, storage: true, cover: "Linen blend", assembly: "None" },
},
{
name: "Folding Camp Chair",
spec: { seatHeight: "45 cm", seatWidth: "48 cm", weight: "3.1 kg", holds: "120 kg", adjusts: false, folds: true, storage: false, cover: "Polyester", assembly: "None" },
},
]
/** The rows, in the order the table groups them. */
export const SPEC_ROWS: readonly { id: keyof SeatSpec; label: string; group: "Size" | "Build" }[] = [
{ id: "seatHeight", label: "Seat height", group: "Size" },
{ id: "seatWidth", label: "Seat width", group: "Size" },
{ id: "weight", label: "Weight", group: "Size" },
{ id: "holds", label: "Holds up to", group: "Build" },
{ id: "adjusts", label: "Adjusts in height", group: "Build" },
{ id: "folds", label: "Folds flat", group: "Build" },
{ id: "storage", label: "Storage inside", group: "Build" },
{ id: "cover", label: "Seat and cover", group: "Build" },
{ id: "assembly", label: "Assembly", group: "Build" },
]
export const RECORD_ROWS = [
{ id: "price", label: "Price" },
{ id: "rated", label: "Rated" },
{ id: "sold", label: `Sold, last ${SHELF_DAYS} days` },
{ id: "stock", label: "In stock" },
] as const
/** A seat as a column: the shelf row and every value the table prints for it. */
export type SeatColumn = Pick<ShelfProduct, "id" | "name" | "category" | "priceCents" | "sold" | "href"> & {
values: Record<string, string | boolean>
}
/** The seats off the shelf, in `SEATS` order, and the one bought most. */
export function seats(): { columns: SeatColumn[]; mostBought: string } {
const shelf = storeShelf()
const columns = SEATS.map(({ name, spec }) => {
const row = shelf.find((entry) => entry.name === name)
if (!row) throw new Error(`product-features-02 compares "${name}", which is not on the shelf`)
const score = formatNumber(row.rating, { minimumFractionDigits: 1, maximumFractionDigits: 1 })
return {
id: row.id,
name: row.name,
category: row.category,
priceCents: row.priceCents,
sold: row.sold,
href: row.href,
values: {
...spec,
price: formatCurrency(row.priceCents / 100, "USD"),
rated: row.ratings ? `${score} from ${row.ratings}` : "Not yet",
sold: formatNumber(row.sold),
stock: row.stockState === "low" ? `Low, ${formatNumber(row.stock)} left` : formatNumber(row.stock),
},
}
})
const mostBought = [...columns].sort((a, b) => b.sold - a.sold || a.name.localeCompare(b.name))[0].id
return { columns, mostBought }
}$
npx shadcn@latest add @vibra/product-features-02What's in the box
product-features-03Everything in one product's box, grouped and counted with what each piece measures, what is not in it, and the box itself — its size, weight and who ships it — under the product's picture.
import * as React from "react"
import Link from "next/link"
import { ArrowRightIcon, PackageIcon } from "lucide-react"
import { cn } from "@/lib/utils"
import { formatNumber } from "@/lib/format"
import { buttonVariants } from "@/components/ui/button"
import { Callout } from "@/components/ui/callout"
import { Card, CardContent, CardFooter } from "@/components/ui/card"
import { Price } from "@/components/ui/price"
import { ProductArt } from "@/components/ui/product-art"
import { BOX, boxedProduct, pieces } from "./product-features-03.data"
const STOCK = { in: "In stock", low: "Low stock", out: "Sold out" } as const
/**
* What comes in one product's box: every piece, grouped and counted, each
* with what it measures; what does not come in it, said plainly; and the box
* itself — its size and weight, and who ships it — under the product's
* picture, with its price and the way to its page.
*
* A server component: the product's figures come from
* `./product-features-03.data`, with the shop's list of what is in the box.
*/
export function ProductFeatures03() {
const titleId = React.useId()
const product = boxedProduct()
const total = pieces()
const [length, width, height] = BOX.box.size
return (
<section
data-section="product-features-03"
data-slot="section"
aria-labelledby={titleId}
className="w-full bg-background px-4 py-16 text-foreground md:px-6 md:py-24"
>
<div className="mx-auto grid max-w-7xl items-start gap-8 lg:grid-cols-[minmax(0,5fr)_minmax(0,7fr)] lg:gap-16">
<Card className="gap-2 p-2 lg:sticky lg:top-6">
<CardContent className="flex-none p-0">
<ProductArt name={product.name} category={product.category} className="rounded-none" />
</CardContent>
<CardFooter className="flex-col items-stretch gap-3 py-3">
<div className="flex items-start gap-3">
<PackageIcon aria-hidden="true" className="mt-0.5 size-4 shrink-0 text-muted-foreground" />
<dl className="grid flex-1 grid-cols-2 gap-x-4 gap-y-2 text-sm">
<div className="flex flex-col gap-0.5">
<dt className="text-xs text-muted-foreground">The box</dt>
<dd className="text-foreground tabular-nums">
{length} <span aria-hidden="true">×</span>
<span className="sr-only">by</span> {width} <span aria-hidden="true">×</span>
<span className="sr-only">by</span> {height} cm
</dd>
</div>
<div className="flex flex-col gap-0.5">
<dt className="text-xs text-muted-foreground">Packed weight</dt>
<dd className="text-foreground tabular-nums">{formatNumber(BOX.box.kilograms)} kg</dd>
</div>
<div className="col-span-2 flex flex-col gap-0.5">
<dt className="text-xs text-muted-foreground">Ships from</dt>
<dd className="text-foreground">{product.supplier}</dd>
</div>
</dl>
</div>
</CardFooter>
</Card>
<div className="flex flex-col gap-8">
<div className="flex flex-col gap-2">
<p className="type-eyebrow">{product.name}</p>
<h2 id={titleId} className="type-display text-2xl text-balance md:text-3xl">
What’s in the box: {total} pieces
</h2>
<p className="text-sm text-pretty text-muted-foreground md:text-base">{BOX.lede}</p>
</div>
<div className="flex flex-col gap-6">
{BOX.contents.map((group) => (
<div key={group.group} className="flex flex-col gap-1">
<h3 className="type-eyebrow">{group.group}</h3>
<ul className="divide-y divide-border border-b border-border">
{group.items.map((item) => (
<li key={item.name} className="flex items-baseline gap-3 py-2.5 text-sm">
<span className="w-6 shrink-0 text-muted-foreground tabular-nums">
{item.count}
<span aria-hidden="true">×</span>
</span>
<span className="font-medium text-foreground">{item.name}</span>{" "}
<span className="ml-auto text-right text-muted-foreground tabular-nums">{item.detail}</span>
</li>
))}
</ul>
</div>
))}
</div>
<Callout title="Not in the box">{BOX.notIncluded}</Callout>
<div className="flex flex-wrap items-center justify-between gap-x-6 gap-y-4 border-t border-border pt-6">
<div className="flex flex-col gap-1">
<Price amount={product.priceCents / 100} size="lg" />
<p className="text-xs text-muted-foreground tabular-nums">
{STOCK[product.stockState]}
{product.stockState === "low" ? `, ${formatNumber(product.stock)} left` : null}
</p>
</div>
<Link href={product.href} className={cn(buttonVariants({ variant: "outline" }), "group/all")}>
See the {product.name.toLowerCase()}
<ArrowRightIcon
aria-hidden="true"
data-icon="inline-end"
className="transition-transform duration-(--duration-fast) ease-(--ease-standard) group-hover/all:translate-x-0.5"
/>
</Link>
</div>
</div>
</div>
</section>
)
}
export default ProductFeatures03/**
* What `product-features-03` reads: one product off the storefront's shelf —
* its price, its stock, who ships it and what buyers rated it — and the
* shop's list of what comes in its box, piece by piece, with what does not
* and the box itself. The list is copy, kept here beside the name it
* describes; every figure about the product is the store's.
*
* Selectors only, reading the store inside functions and run on the server.
*/
import { storeShelf, type ShelfProduct } from "@/lib/dashboards/ecommerce/storefront"
/** One piece in the box: how many, what it is and what it measures. */
export type BoxItem = { count: number; name: string; detail: string }
/** What the box holds, grouped; what it does not; and the box itself, in centimetres and kilograms. */
export type Box = {
lede: string
contents: readonly { group: string; items: readonly BoxItem[] }[]
notIncluded: string
box: { size: readonly [number, number, number]; kilograms: number }
}
/** The product the band shows, and what comes in its box: change them together. */
export const PRODUCT = "Knife Block Set"
export const BOX: Box = {
lede: "Five knives, two tools and the block that holds them, each piece in a slot of its own.",
contents: [
{
group: "Knives",
items: [
{ count: 1, name: "Chef's knife", detail: "20 cm blade" },
{ count: 1, name: "Bread knife", detail: "20 cm blade" },
{ count: 1, name: "Santoku", detail: "17 cm blade" },
{ count: 1, name: "Utility knife", detail: "13 cm blade" },
{ count: 1, name: "Paring knife", detail: "9 cm blade" },
],
},
{
group: "Tools",
items: [
{ count: 1, name: "Kitchen shears", detail: "Come apart to wash" },
{ count: 1, name: "Honing steel", detail: "23 cm rod" },
],
},
{
group: "The block",
items: [{ count: 1, name: "Bamboo block", detail: "Seven slots, 24 cm tall" }],
},
],
notIncluded:
"A whetstone. The honing steel keeps each edge straight between uses; a stone puts the edge back, about once a year.",
box: { size: [42, 18, 10], kilograms: 2.3 },
}
/** Every piece in the box, counted. */
export const pieces = () => BOX.contents.reduce((sum, group) => sum + group.items.reduce((total, item) => total + item.count, 0), 0)
/** The product as the band shows it. */
export type BoxedProduct = Pick<
ShelfProduct,
"id" | "name" | "category" | "supplier" | "priceCents" | "stock" | "stockState" | "rating" | "ratings" | "href"
>
/** `PRODUCT` off the shelf. */
export function boxedProduct(): BoxedProduct {
const product = storeShelf().find((row) => row.name === PRODUCT)
if (!product) throw new Error(`product-features-03 shows "${PRODUCT}", which is not on the shelf`)
const { id, name, category, supplier, priceCents, stock, stockState, rating, ratings, href } = product
return { id, name, category, supplier, priceCents, stock, stockState, rating, ratings, href }
}$
npx shadcn@latest add @vibra/product-features-03