Add Phase 1 Pocket Pascal UI: home, pantry, week plan, and plate.

Replace the hello-world shell with the elevated design system, bilingual screens, recipe/extras data, and the interactive Mein Teller view.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Steffi Müller
2026-08-04 12:22:32 +02:00
parent 3785d08e97
commit a482bf5b09
60 changed files with 7366 additions and 72 deletions

139
client/src/lib/catalog.ts Normal file
View File

@@ -0,0 +1,139 @@
import type {
CategoryId,
CatalogItem,
ExtraItem,
FoodCategory,
FoodItem,
LocalizedString,
PlanRecipeType,
Recipe,
TagType,
TimeSlot,
} from "@/types/domain"
import extrasRaw from "@/data/extras.json"
import foodsRaw from "@/data/foods.json"
import recipesRaw from "@/data/recipes.json"
import stepsRaw from "@/data/steps.json"
interface RawFoodItem {
id: string
name: LocalizedString
v: LocalizedString
f?: TagType
approx?: boolean
}
interface RawFoodCategory {
id: CategoryId
cls: string
title: LocalizedString
thumb: LocalizedString
items: RawFoodItem[]
}
export interface PlateStep {
n: number
cls: string
title: LocalizedString
qty: LocalizedString
text: LocalizedString
}
function mapItem(cat: CategoryId, item: RawFoodItem): FoodItem {
return {
id: item.id,
cat,
name: item.name,
valueInfo: item.v,
tag: item.f,
isApprox: item.approx,
}
}
export const foodCategories: FoodCategory[] = (foodsRaw as RawFoodCategory[]).map(
(category) => ({
id: category.id,
cls: category.cls,
title: category.title,
thumb: category.thumb,
items: category.items.map((item) => mapItem(category.id, item)),
}),
)
export const foods: FoodItem[] = foodCategories.flatMap((category) => category.items)
export const foodById: Record<string, FoodItem> = Object.fromEntries(
foods.map((item) => [item.id, item]),
)
export const extras: ExtraItem[] = extrasRaw as ExtraItem[]
export const extraById: Record<string, ExtraItem> = Object.fromEntries(
extras.map((item) => [item.id, item]),
)
export const recipes: Recipe[] = recipesRaw as Recipe[]
export const plateSteps: PlateStep[] = stepsRaw as PlateStep[]
export const PLAN_TYPE_ORDER: PlanRecipeType[] = [
"breakfast",
"snack",
"full",
"recipe",
]
export function foodsInCategory(cat: CategoryId): FoodItem[] {
return foodCategories.find((category) => category.id === cat)?.items ?? []
}
export function resolveCatalogId(id: string): CatalogItem | null {
const food = foodById[id]
if (food) {
return {
id: food.id,
name: food.name,
source: "cat",
cat: food.cat,
valueInfo: food.valueInfo,
tag: food.tag,
}
}
const extra = extraById[id]
if (extra) {
return {
id: extra.id,
name: extra.name,
source: "extra",
}
}
return null
}
export function resolveRecipeUses(recipe: Recipe): CatalogItem[] {
return recipe.uses
.map((id) => resolveCatalogId(id))
.filter((item): item is CatalogItem => Boolean(item))
}
export function recipesByType(type: PlanRecipeType): Recipe[] {
return recipes.filter((recipe) => recipe.type === type)
}
/** Map plan recipe types onto suggestion time slots. */
export function timeSlotsForRecipe(recipe: Recipe): TimeSlot[] {
switch (recipe.type) {
case "breakfast":
return ["morning"]
case "snack":
return ["afternoon"]
case "full":
return ["midday", "evening"]
case "recipe":
return ["morning", "afternoon", "midday", "evening"]
}
}
export function mealTypeForRecipe(recipe: Recipe): "full" | "half" {
return recipe.type === "full" ? "full" : "half"
}