Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## 2026-02-13

### Added

- Metric to imperial conversion for recipe ingredient units

### Changed

- Tuned prompt to remove ingredient preparation from recipe ingredients
Expand Down
Original file line number Diff line number Diff line change
@@ -1,18 +1,21 @@
import React from "react";
import { Row } from "react-bootstrap";
import { IngredientRequirement, MeasureUnit } from "src/shared/CookTime";
import { UnitPreference } from "src/shared/units";
import { IngredientDisplay } from "../Ingredients/IngredientDisplay";

interface IngredientRequirementListProps {
ingredientRequirements: IngredientRequirement[];
units: MeasureUnit[];
multiplier: number;
unitPreference: UnitPreference;
}

export function IngredientRequirementList({
ingredientRequirements,
units,
multiplier,
unitPreference,
}: IngredientRequirementListProps) {
return (
<>
Expand All @@ -23,6 +26,7 @@ export function IngredientRequirementList({
<IngredientDisplay
showAlternatUnit={true}
units={units}
unitPreference={unitPreference}
ingredientRequirement={{ ...ingredient, quantity: scaledQuantity }}
/>
</Row>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,36 +1,35 @@
import React from "react";
import { IngredientRequirement, MeasureUnit } from "src/shared/CookTime";
import { UnitPreference, convertQuantity, formatNumber, formatUnitName } from "src/shared/units";

type IngredientDisplayProps = {
ingredientRequirement: IngredientRequirement
strikethrough?: boolean,
showAlternatUnit?: boolean
units?: MeasureUnit[]
unitPreference?: UnitPreference
}
export class IngredientDisplay extends React.Component<IngredientDisplayProps, {}> {

render() {
let ingredient = this.props.ingredientRequirement.ingredient
var unitName = ""
switch (this.props.ingredientRequirement.unit) {
case "count":
unitName = ""
break;
const unitPreference = this.props.unitPreference ?? "recipe";
const conversion = convertQuantity({
quantity: this.props.ingredientRequirement.quantity,
unitName: this.props.ingredientRequirement.unit,
units: this.props.units,
preference: unitPreference,
});

case "fluid_ounce":
unitName = "fluid ounce"
break;

default:
unitName = this.props.ingredientRequirement.unit.toLowerCase()
break;
}
var quantity = <>{this.props.ingredientRequirement.quantity.toString()}</>
let fraction = this.Fraction(this.props.ingredientRequirement.quantity);
const unitName = conversion.unitName === "count" ? "" : formatUnitName(conversion.unitName);
let fraction = this.Fraction(conversion.quantity);
let quantity = unitPreference === "metric"
? <>{formatNumber(conversion.quantity)}</>
: <>{fraction}</>;

// Show IR text, but if that's not available then show the ingredient canonical name.
var ingredientName = (this.props.ingredientRequirement.text ?? ingredient.name.split(";").map(s => s.trim())[0]).toLowerCase()
var text = <>{fraction} {unitName} {this.props.showAlternatUnit ? this.getAlternateUnit() : null} {ingredientName}
var text = <>{quantity} {unitName} {this.props.showAlternatUnit && unitPreference === "recipe" ? this.getAlternateUnit() : null} {ingredientName}
</>
if (this.props.strikethrough) {
text = <s>{text}</s>
Expand Down Expand Up @@ -131,4 +130,4 @@ export class IngredientDisplay extends React.Component<IngredientDisplayProps, {
return ""
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ export function RecipeComponentEditor({ component, componentIndex }: RecipeCompo
recipe,
edit,
units,
unitPreference,
newServings,
updateComponent,
appendIngredientToComponent,
Expand Down Expand Up @@ -86,6 +87,7 @@ export function RecipeComponentEditor({ component, componentIndex }: RecipeCompo
<IngredientRequirementList
ingredientRequirements={component.ingredients ?? []}
units={units}
unitPreference={unitPreference}
multiplier={newServings / recipe.servingsProduced}
/>
)}
Expand All @@ -102,6 +104,8 @@ export function RecipeComponentEditor({ component, componentIndex }: RecipeCompo
recipe={recipe}
component={component}
newServings={newServings}
unitPreference={unitPreference}
units={units}
edit={edit}
onDeleteStep={(idx) => deleteStepFromComponent(componentIndex, idx)}
onChange={(newSteps) => updateStepsInComponent(componentIndex, newSteps)}
Expand Down
15 changes: 15 additions & 0 deletions src/CookTime/client-app/src/components/Recipe/RecipeContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import {
toRecipeUpdateDto,
addToList,
} from 'src/shared/CookTime';
import { UnitPreference } from 'src/shared/units';

export type PendingImage = {
id: string;
Expand All @@ -35,6 +36,7 @@ interface RecipeContextState {
imageOperationInProgress: boolean;
edit: boolean;
units: MeasureUnit[];
unitPreference: UnitPreference;
newServings: number;
errorMessage: string | null;
operationInProgress: boolean;
Expand All @@ -51,6 +53,7 @@ interface RecipeContextActions {
setErrorMessage: (message: string | null) => void;
setNewServings: (servings: number) => void;
setToastMessage: (message: string | null) => void;
setUnitPreference: (preference: UnitPreference) => void;

// Recipe updates
updateRecipe: (updates: Partial<MultiPartRecipe>) => void;
Expand Down Expand Up @@ -129,6 +132,7 @@ export function RecipeProvider({ recipeId, generatedRecipe, children }: RecipePr
const [imageOperationInProgress, setImageOperationInProgress] = useState(false);
const [edit, setEdit] = useState(false);
const [units, setUnits] = useState<MeasureUnit[]>([]);
const [unitPreference, setUnitPreference] = useState<UnitPreference>('recipe');
const [newServings, setNewServings] = useState(1);
const [errorMessage, setErrorMessage] = useState<string | null>(null);
const [operationInProgress, setOperationInProgress] = useState(false);
Expand Down Expand Up @@ -175,6 +179,11 @@ export function RecipeProvider({ recipeId, generatedRecipe, children }: RecipePr

// Load initial data
useEffect(() => {
const savedPreference = window.localStorage.getItem('cooktime.unitPreference');
if (savedPreference === 'recipe' || savedPreference === 'imperial' || savedPreference === 'metric') {
setUnitPreference(savedPreference);
}

// Fetch units
fetch('/api/recipe/units')
.then((res) => res.json())
Expand Down Expand Up @@ -223,6 +232,10 @@ export function RecipeProvider({ recipeId, generatedRecipe, children }: RecipePr
.then((result) => setNutritionFacts(result as RecipeNutritionFacts));
}, [recipeId, generatedRecipe, applyGeneratedRecipe]);

useEffect(() => {
window.localStorage.setItem('cooktime.unitPreference', unitPreference);
}, [unitPreference]);

// Recipe updates
const updateRecipe = useCallback((updates: Partial<MultiPartRecipe>) => {
setRecipe((prev) => ({ ...prev, ...updates }));
Expand Down Expand Up @@ -521,6 +534,7 @@ export function RecipeProvider({ recipeId, generatedRecipe, children }: RecipePr
imageOperationInProgress,
edit,
units,
unitPreference,
newServings,
errorMessage,
operationInProgress,
Expand All @@ -534,6 +548,7 @@ export function RecipeProvider({ recipeId, generatedRecipe, children }: RecipePr
setErrorMessage,
setNewServings,
setToastMessage,
setUnitPreference,
updateRecipe,
updateComponent,
appendIngredientToComponent,
Expand Down
43 changes: 42 additions & 1 deletion src/CookTime/client-app/src/components/Recipe/RecipeFields.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import React from 'react';
import { Button, Col, Form, Row } from 'react-bootstrap';
import { Button, Col, DropdownButton, Dropdown, Form, Row } from 'react-bootstrap';
import { useRecipeContext } from './RecipeContext';
import { ImageEditor } from './ImageEditor';
import { Tags } from '../Tags/Tags';
import { UnitPreference } from 'src/shared/units';

export function RecipeFields() {
const {
Expand All @@ -14,8 +15,10 @@ export function RecipeFields() {
pendingImages,
imageOrder,
imageOperationInProgress,
unitPreference,
setNewServings,
updateRecipe,
setUnitPreference,
handleAddImages,
handleRemoveExistingImage,
handleRemovePendingImage,
Expand Down Expand Up @@ -223,10 +226,48 @@ export function RecipeFields() {
);
};

const renderUnitPreference = () => (
<Row className="padding-right-0 d-flex align-items-center recipe-edit-row">
<Col className="col-3 recipe-field-title">Units</Col>
<Col className="col d-flex align-items-center">
<DropdownButton
variant="secondary"
title={
unitPreference === 'imperial'
? 'Imperial'
: unitPreference === 'metric'
? 'Metric'
: 'Recipe'
}
>
<Dropdown.Item
active={unitPreference === 'recipe'}
onClick={() => setUnitPreference('recipe' as UnitPreference)}
>
Recipe
</Dropdown.Item>
<Dropdown.Item
active={unitPreference === 'imperial'}
onClick={() => setUnitPreference('imperial' as UnitPreference)}
>
Imperial
</Dropdown.Item>
<Dropdown.Item
active={unitPreference === 'metric'}
onClick={() => setUnitPreference('metric' as UnitPreference)}
>
Metric
</Dropdown.Item>
</DropdownButton>
</Col>
</Row>
);

return (
<div>
{renderCaloriesPerServing()}
{renderServings()}
{renderUnitPreference()}
{renderCategories()}
{renderCookTime()}
{renderSource()}
Expand Down
16 changes: 13 additions & 3 deletions src/CookTime/client-app/src/components/Recipe/RecipeStep.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { OverlayTrigger, Tooltip } from "react-bootstrap";
import { IngredientRequirement, MultiPartRecipe, Recipe, RecipeComponent } from "src/shared/CookTime";
import { IngredientRequirement, MeasureUnit, MultiPartRecipe, Recipe, RecipeComponent } from "src/shared/CookTime";
import { UnitPreference, convertQuantity, formatNumber, formatUnitName } from "src/shared/units";

type Segment = {
ingredient: IngredientRequirement | null,
Expand All @@ -12,6 +13,8 @@ interface StepProps {
multipart: boolean;
component?: RecipeComponent;
newServings: number;
unitPreference: UnitPreference;
units: MeasureUnit[];
}

function trifurcate(s: string, position: number, length: number) {
Expand All @@ -22,7 +25,7 @@ function trifurcate(s: string, position: number, length: number) {
};
}

export function Step({ recipe, stepText, multipart, component, newServings }: StepProps) {
export function Step({ recipe, stepText, multipart, component, newServings, unitPreference, units }: StepProps) {
let segments: Segment[] = [{ ingredient: null, text: stepText }];

const ingredientRequirements: IngredientRequirement[] = multipart
Expand Down Expand Up @@ -81,7 +84,14 @@ export function Step({ recipe, stepText, multipart, component, newServings }: St
}

const newQuantity = segment.ingredient.quantity * newServings / recipe.servingsProduced;
const tooltipTitle = `${newQuantity} ${segment.ingredient.unit}`;
const conversion = convertQuantity({
quantity: newQuantity,
unitName: segment.ingredient.unit,
units,
preference: unitPreference,
});
const unitLabel = conversion.unitName === "count" ? "" : formatUnitName(conversion.unitName);
const tooltipTitle = `${formatNumber(conversion.quantity)}${unitLabel ? ` ${unitLabel}` : ""}`;

return (
<OverlayTrigger
Expand Down
11 changes: 9 additions & 2 deletions src/CookTime/client-app/src/components/Recipe/RecipeStepList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,14 @@ import { Step } from "./RecipeStep";
import { DndContext, closestCenter, KeyboardSensor, PointerSensor, useSensor, useSensors, DragEndEvent } from "@dnd-kit/core";
import { SortableContext, sortableKeyboardCoordinates, useSortable, verticalListSortingStrategy, arrayMove } from "@dnd-kit/sortable";
import { CSS } from "@dnd-kit/utilities";
import { MultiPartRecipe, Recipe, RecipeComponent } from "src/shared/CookTime";
import { MeasureUnit, MultiPartRecipe, Recipe, RecipeComponent } from "src/shared/CookTime";
import { UnitPreference } from "src/shared/units";

type RecipeStepListProps = {
recipe: Recipe | MultiPartRecipe,
newServings: number,
unitPreference: UnitPreference,
units: MeasureUnit[],
multipart: boolean,
component?: RecipeComponent,
onDeleteStep: (i: number, component?: RecipeComponent) => void,
Expand Down Expand Up @@ -64,7 +67,7 @@ function SortableStep({ id, index, stepText, onTextChange, onDelete }: SortableS
}

export function RecipeStepList(props: RecipeStepListProps) {
const { recipe, multipart, component, newServings, edit, onChange, onDeleteStep, onNewStep } = props;
const { recipe, multipart, component, newServings, unitPreference, units, edit, onChange, onDeleteStep, onNewStep } = props;

const sensors = useSensors(
useSensor(PointerSensor),
Expand Down Expand Up @@ -138,6 +141,8 @@ export function RecipeStepList(props: RecipeStepListProps) {
multipart={multipart}
recipe={recipe}
stepText={step}
unitPreference={unitPreference}
units={units}
newServings={newServings} />
</Col>
</Row>
Expand All @@ -156,6 +161,8 @@ export function RecipeStepList(props: RecipeStepListProps) {
recipe={recipe}
stepText={step}
component={component}
unitPreference={unitPreference}
units={units}
newServings={newServings} />
</Col>
</Row>
Expand Down
Loading