DoughLab is a small React + TypeScript playground for calculating pizza dough formulas based on pan shape, pan size, and pizza style. It focuses more on logic and architecture than on visual polish, and uses a typed, reusable form system plus global state with Zustand.
- Lets you select:
- Shape:
NONE,CIRCLE,RECTANGLE,SQUARE - Style:
NONE,NYC,CHICAGO,MARGHERITA,NAPOLETANA,DETROIT,SICILIAN,GRANDMA - Size: depends on shape (e.g.
10"/12"for circles,6x6/9x9for rectangles, etc.)
- Shape:
- Computes:
- Pan area based on shape + size
- Total dough weight from style-specific dough density
- Flour / water / salt / yeast / oil using baker’s percentages
- Displays results as:
- Two side‑by‑side cards: one Metric (g), one Imperial (oz)
- Left‑aligned labels, right‑aligned values in both columns
The calculation pipeline is intentionally simple and explicit.
The function:
calculateArea(input)takes a discriminated union based on shape (roughly):
CIRCLE→{ shape: "CIRCLE"; diameter: number }SQUARE→{ shape: "SQUARE"; side: number }RECTANGLE→{ shape: "RECTANGLE"; width: number; height: number }
Formulas used:
- Circle:
area = π × (diameter / 2)² - Square:
area = side × side - Rectangle:
area = width × height
This keeps the logic explicit and makes it impossible (at the type level) to “forget” a dimension for a shape.
Each pizza style has an associated dough density (grams of dough per square unit of pan area). Conceptually:
const doughDensityMap = {
NYC: 3.8,
CHICAGO: 6.2,
MARGHERITA: 3.2,
NAPOLETANA: 3.1,
DETROIT: 5.5,
SICILIAN: 4.8,
GRANDMA: 4.3,
// NONE -> treated as 0 (no valid style selected)
};The function:
doughDensity(style)returns the numeric density for a given style. If the style is NONE, the density is treated as 0 so calculations produce a “no result” state.
Once you have area A and density D, you get a target total dough weight:
doughWeight = area * density;This is the base for all ingredient calculations.
The function:
calculateDoughIngredients(area, style, hydration, salt, yeast, oil)does the following:
-
Get
density = doughDensity(style) -
Compute
doughWeight = area * density -
Use baker’s percentages:
hydration→ default0.65(65% water)salt→ default0.03(3% salt)yeast→ default0.01(1% yeast)oil→ default0.02(2% oil)
-
Total percentage factor:
factor = 1 + hydration + salt + yeast + oil;
-
Base flour weight:
flour = doughWeight / factor;
-
Other ingredients:
water = flour * hydration; salt = flour * saltPct; yeast = flour * yeastPct; oil = flour * oilPct;
The function returns a structured object with:
{
doughWeight,
flour,
water,
salt,
yeast,
oil
}App‑level code then converts those into:
- Metric values in grams (with
toFixed(1)) - Imperial values in ounces (
g / 28.3495,toFixed(2))
These are rendered into two side‑by‑side “Result” cards.
Instead of lifting React state up through multiple components, DoughLab uses a small Zustand store:
interface PizzaState {
shape: PizzaShape;
style: PizzaStyle;
size: PizzaSizeOption;
setShape: (shape: PizzaShape) => void;
setStyle: (style: PizzaStyle) => void;
setSize: (size: PizzaSizeOption) => void;
}Key points:
shape,style, andsizeare all stored centrally.- Selecting a shape can reset the size when needed (e.g. when shape changes, previous size may no longer be valid).
- Components like
ShapeSelector,StyleSelector, andSizeSelectorsubscribe only to the slice of state they care about.
This keeps the app simple while still showing a realistic global state pattern.
Instead of using native <select> elements everywhere, the app uses a custom generic select component that is reused for shape, style, and size.
Each model uses the pattern:
export const PizzaShape = ['NONE', 'CIRCLE', 'RECTANGLE', 'SQUARE'] as const;
export type PizzaShape = (typeof PizzaShape)[number];Same idea for PizzaStyle and the size options.
This lets the select component stay strongly typed without hard‑coding specific enums.
The form select is defined roughly as:
interface FormSelectProps<T extends string> {
label: string;
options: readonly T[];
value: T;
onChange: (value: T) => void;
}And implemented as a custom dropdown using:
useState→ open/close stateuseRef/useEffect→ close on outside click- A mapped list of options with click handlers
Because it’s generic (<T extends string>), it can be reused for any of:
PizzaShapePizzaStylePizzaSizeOption
Usage example:
<FormSelect
label="Pizza Shape"
options={PizzaShape}
value={shape}
onChange={setShape}
/>This shows how to build a lightweight, type‑safe, reusable select component without dragging in a full UI framework.
The main layout is:
- A form grid for the three selectors
- A conditional render for the results:
- If
shape,style, andsizeare all valid → show results - Otherwise → show an instructional message
- If
The result display is extracted into its own Results component, which:
- Accepts the raw calculation result object as props
- Builds a
rowsarray with{ label, metric, imperial } - Renders two cards:
- Left card labeled “Metric”
- Right card labeled “Imperial”
- Uses
flexwithjustify-betweenon each row for left‑aligned labels and right‑aligned values in both cards.
src/
├─ components/
│ ├─ FormSelect/
│ │ └─ FormSelect.tsx
│ ├─ ShapeSelector/
│ ├─ SizeSelector/
│ ├─ StyleSelector/
│ └─ Results/
├─ models/
│ ├─ AreaInput.ts
│ ├─ DoughBreakdown.ts
│ ├─ PizzaShape.ts
│ ├─ PizzaStyle.ts
│ └─ PizzaSize.ts
├─ stores/
│ └─ pizzaStore.ts
├─ utils/
│ ├─ doughCalculator.ts
│ ├─ resolveAreaInput.ts
│ └─ sizeOptionsForShape.ts
└─ App.tsxgit clone https://github.com/Travisaurus-Rex/dough-lab/
cd doughlab
npm install
npm run devOpen:
http://localhost:5173/Then:
- Pick a shape
- Pick a size (valid options depend on shape)
- Pick a style
- Read the dough formula in both metric and imperial.
- Custom hydration, salt, yeast, and oil sliders
- Support for multiple pizzas / batch calculations
- Saving named presets (e.g., “Friday Night Detroit”)
- Export to text / clipboard / PDF
- Proper responsive design and theming
- Inline graph showing dough weight vs. pan size
MIT — do whatever, improve it, break it, fork it.
