Product figures
Price, stock against the reorder point, and units and revenue over twelve weeks against the twelve before; reads productRecord(id).
Preview
"use client"
import { formatCurrency, formatNumber } from "@/lib/format"
import { StatCard } from "@/components/ui/stat-card"
import { StatCardGroup } from "@/components/ui/stat-card-group"
import { useProduct } from "./product-context"
export type ProductFiguresProps = {
reorderPoint: number
weeks: number
sales: { units: number; revenue: number }
previous: { units: number; revenue: number }
}
/** A change as a ratio, or nothing when there is no earlier figure to divide by — a rise from nothing is no percentage. */
const change = (now: number, before: number) => (before === 0 ? (now === 0 ? 0 : undefined) : now / before - 1)
/**
* The four figures under the title. Price and stock come from the provider,
* so a save in the form below moves them; units and revenue are history, read
* once on the server and measured against the weeks before. The stock tile
* tints its frame once the shelf is at or below the reorder point.
*/
export function ProductFigures({ reorderPoint, weeks, sales, previous }: ProductFiguresProps) {
const { figures } = useProduct()
const low = figures.stock <= reorderPoint
const span = `last ${weeks} weeks`
return (
<StatCardGroup data-widget="widget-ecommerce-product-product-figures" columns={4}>
<StatCard label="Price" value={formatCurrency(figures.priceCents / 100)} description="list price" />
<StatCard
label="In stock"
value={formatNumber(figures.stock)}
description={`reorder at ${reorderPoint}`}
tone={low ? "warning" : "neutral"}
/>
<StatCard
label="Units sold"
value={formatNumber(sales.units)}
delta={change(sales.units, previous.units)}
description={span}
/>
<StatCard
label="Revenue"
value={formatCurrency(sales.revenue, "USD", { maximumFractionDigits: 0 })}
delta={change(sales.revenue, previous.revenue)}
description={`${span}, at the prices charged`}
/>
</StatCardGroup>
)
}Install
$
npx shadcn@latest add @vibra/widget-ecommerce-product-product-figuresNeeds the @vibra registry in your components.json — set it up once.
Source
"use client"
import { formatCurrency, formatNumber } from "@/lib/format"
import { StatCard } from "@/components/ui/stat-card"
import { StatCardGroup } from "@/components/ui/stat-card-group"
import { useProduct } from "./product-context"
export type ProductFiguresProps = {
reorderPoint: number
weeks: number
sales: { units: number; revenue: number }
previous: { units: number; revenue: number }
}
/** A change as a ratio, or nothing when there is no earlier figure to divide by — a rise from nothing is no percentage. */
const change = (now: number, before: number) => (before === 0 ? (now === 0 ? 0 : undefined) : now / before - 1)
/**
* The four figures under the title. Price and stock come from the provider,
* so a save in the form below moves them; units and revenue are history, read
* once on the server and measured against the weeks before. The stock tile
* tints its frame once the shelf is at or below the reorder point.
*/
export function ProductFigures({ reorderPoint, weeks, sales, previous }: ProductFiguresProps) {
const { figures } = useProduct()
const low = figures.stock <= reorderPoint
const span = `last ${weeks} weeks`
return (
<StatCardGroup data-widget="widget-ecommerce-product-product-figures" columns={4}>
<StatCard label="Price" value={formatCurrency(figures.priceCents / 100)} description="list price" />
<StatCard
label="In stock"
value={formatNumber(figures.stock)}
description={`reorder at ${reorderPoint}`}
tone={low ? "warning" : "neutral"}
/>
<StatCard
label="Units sold"
value={formatNumber(sales.units)}
delta={change(sales.units, previous.units)}
description={span}
/>
<StatCard
label="Revenue"
value={formatCurrency(sales.revenue, "USD", { maximumFractionDigits: 0 })}
delta={change(sales.revenue, previous.revenue)}
description={`${span}, at the prices charged`}
/>
</StatCardGroup>
)
}"use client"
import * as React from "react"
/** The two numbers the edit form can change, held once for every card that prints them. */
export type ProductFigures = { priceCents: number; stock: number }
type ProductContextValue = {
figures: ProductFigures
setFigures: (next: ProductFigures) => void
}
const ProductContext = React.createContext<ProductContextValue | null>(null)
/**
* Holds the price and the stock the server last confirmed. The page renders
* them from the row it read; a save replaces them with what the action wrote
* back, so the figures at the top and the form at the bottom never disagree.
*/
export function ProductProvider({ initial, children }: { initial: ProductFigures; children: React.ReactNode }) {
const [figures, setFigures] = React.useState(initial)
const value = React.useMemo(() => ({ figures, setFigures }), [figures])
return <ProductContext.Provider value={value}>{children}</ProductContext.Provider>
}
export function useProduct(): ProductContextValue {
const context = React.useContext(ProductContext)
if (!context) throw new Error("useProduct must be used inside <ProductProvider>.")
return context
}Its page
On its page the card sits among the rest of the dashboard and shares its range and its data with them.
From the Product record page