From 07c17d13593ff54989b46b81ed75a21487ca1ee3 Mon Sep 17 00:00:00 2001 From: David Date: Thu, 20 Aug 2026 21:26:16 +0200 Subject: [PATCH] Initial copy --- .claude/settings.local.json | 8 + .dockerignore | 5 + .github/workflows/docker-publish.yml | 53 +++++ Dockerfile | 6 + app.js | 228 +++++++++++++++++++++ index.html | 20 ++ pizza_recipes.yaml | 295 +++++++++++++++++++++++++++ recipes.json | 80 ++++++++ recipes/biga_pizza_dough.md | 14 ++ recipes/neapolitan_pizza_dough.md | 7 + recipes/new_york_pizza_dough.md | 8 + recipes/sicilian_pizza_dough.md | 7 + style.css | 175 ++++++++++++++++ 13 files changed, 906 insertions(+) create mode 100644 .claude/settings.local.json create mode 100644 .dockerignore create mode 100644 .github/workflows/docker-publish.yml create mode 100644 Dockerfile create mode 100644 app.js create mode 100644 index.html create mode 100644 pizza_recipes.yaml create mode 100644 recipes.json create mode 100644 recipes/biga_pizza_dough.md create mode 100644 recipes/neapolitan_pizza_dough.md create mode 100644 recipes/new_york_pizza_dough.md create mode 100644 recipes/sicilian_pizza_dough.md create mode 100644 style.css diff --git a/.claude/settings.local.json b/.claude/settings.local.json new file mode 100644 index 0000000..51436c9 --- /dev/null +++ b/.claude/settings.local.json @@ -0,0 +1,8 @@ +{ + "permissions": { + "allow": [ + "Bash(chromium-cli --session pizza)", + "WebSearch" + ] + } +} diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..8ab90b3 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,5 @@ +.git +.claude +pizza_recipes.yaml +Dockerfile +.dockerignore diff --git a/.github/workflows/docker-publish.yml b/.github/workflows/docker-publish.yml new file mode 100644 index 0000000..6693045 --- /dev/null +++ b/.github/workflows/docker-publish.yml @@ -0,0 +1,53 @@ +name: Build and push Docker image + +on: + push: + branches: [main] + tags: ["v*.*.*"] + workflow_dispatch: + +env: + REGISTRY: ghcr.io + +jobs: + build-and-push: + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Set lowercase image name + run: echo "IMAGE_NAME=$(echo '${{ github.repository }}' | tr '[:upper:]' '[:lower:]')" >> "$GITHUB_ENV" + + - name: Log in to GHCR + uses: docker/login-action@v3 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Extract metadata + id: meta + uses: docker/metadata-action@v5 + with: + images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} + tags: | + type=raw,value=latest,enable={{is_default_branch}} + type=semver,pattern={{version}} + type=sha,format=short + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Build and push + uses: docker/build-push-action@v6 + with: + context: . + push: true + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + cache-from: type=gha + cache-to: type=gha,mode=max diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..a411161 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,6 @@ +FROM nginx:alpine + +COPY index.html style.css app.js recipes.json /usr/share/nginx/html/ +COPY recipes/ /usr/share/nginx/html/recipes/ + +EXPOSE 80 diff --git a/app.js b/app.js new file mode 100644 index 0000000..b4452ed --- /dev/null +++ b/app.js @@ -0,0 +1,228 @@ +const RECIPES_URL = "recipes.json"; + +const state = { + recipes: [], + selectedId: null, + sliderValues: {}, // { [recipeId]: { hydration, doughBalls, doughBallSizeG } } + markdownCache: {}, // { [recipeId]: htmlString } +}; + +function escapeHtml(str) { + return str + .replace(/&/g, "&") + .replace(//g, ">"); +} + +function inlineMarkdown(text) { + return escapeHtml(text).replace(/\*\*(.+?)\*\*/g, "$1"); +} + +function markdownToHtml(md) { + const lines = md.split("\n"); + let html = ""; + let inList = false; + + const closeList = () => { + if (inList) { + html += ""; + inList = false; + } + }; + + for (const rawLine of lines) { + const line = rawLine.trim(); + if (!line) { + closeList(); + continue; + } + const headingMatch = line.match(/^(#{1,6})\s+(.*)$/); + if (headingMatch) { + closeList(); + const level = Math.min(headingMatch[1].length + 1, 6); // ## -> h3 + html += `${inlineMarkdown(headingMatch[2])}`; + continue; + } + const listMatch = line.match(/^\d+\.\s+(.*)$/); + if (listMatch) { + if (!inList) { + html += "
    "; + inList = true; + } + html += `
  1. ${inlineMarkdown(listMatch[1])}
  2. `; + continue; + } + closeList(); + html += `

    ${inlineMarkdown(line)}

    `; + } + closeList(); + return html; +} + +function computeIngredients(recipe, values) { + const scaleFactor = + (values.doughBalls * values.doughBallSizeG) / + (recipe.baseDoughBalls * recipe.baseDoughBallSizeG); + + const baseFlour = recipe.ingredients.find((i) => i.role === "flour").baseAmount; + const baseWater = recipe.ingredients.find((i) => i.role === "water").baseAmount; + const flourWaterSum = (baseFlour + baseWater) * scaleFactor; + const flour = flourWaterSum / (1 + values.hydration / 100); + const water = flourWaterSum - flour; + + return recipe.ingredients.map((ing) => { + let amount; + if (ing.role === "flour") amount = flour; + else if (ing.role === "water") amount = water; + else amount = ing.baseAmount * scaleFactor; + return { ...ing, amount: Math.round(amount) }; + }); +} + +function renderPicker() { + const nav = document.getElementById("recipe-picker"); + nav.innerHTML = ""; + for (const recipe of state.recipes) { + const btn = document.createElement("button"); + btn.textContent = recipe.name; + btn.className = recipe.id === state.selectedId ? "active" : ""; + btn.addEventListener("click", () => selectRecipe(recipe.id)); + nav.appendChild(btn); + } +} + +function baseHydration(recipe) { + const flour = recipe.ingredients.find((i) => i.role === "flour").baseAmount; + const water = recipe.ingredients.find((i) => i.role === "water").baseAmount; + return Math.round((water / flour) * 100); +} + +async function getInstructionsHtml(recipe) { + if (state.markdownCache[recipe.id]) return state.markdownCache[recipe.id]; + const res = await fetch(recipe.instructionsFile); + if (!res.ok) throw new Error(`Failed to load ${recipe.instructionsFile}`); + const md = await res.text(); + const html = markdownToHtml(md); + state.markdownCache[recipe.id] = html; + return html; +} + +function renderIngredients(recipe) { + const values = state.sliderValues[recipe.id]; + const ingredients = computeIngredients(recipe, values); + const list = ingredients + .map( + (ing) => + `
  3. ${escapeHtml(ing.name)}${ing.amount}${ing.unit}
  4. ` + ) + .join(""); + const container = document.getElementById("ingredients-list"); + if (container) container.innerHTML = list; +} + +function onSliderChange(recipe) { + const panel = document.getElementById("recipe-panel"); + const hydrationInput = panel.querySelector("#slider-hydration"); + const ballsInput = panel.querySelector("#slider-balls"); + const sizeInput = panel.querySelector("#slider-size"); + + const values = { + hydration: Number(hydrationInput.value), + doughBalls: Number(ballsInput.value), + doughBallSizeG: Number(sizeInput.value), + }; + state.sliderValues[recipe.id] = values; + + panel.querySelector("#value-hydration").textContent = `${values.hydration}%`; + panel.querySelector("#value-balls").textContent = values.doughBalls; + panel.querySelector("#value-size").textContent = `${values.doughBallSizeG}g`; + + renderIngredients(recipe); +} + +async function renderRecipePanel(recipe) { + const panel = document.getElementById("recipe-panel"); + + if (!state.sliderValues[recipe.id]) { + state.sliderValues[recipe.id] = { + hydration: baseHydration(recipe), + doughBalls: recipe.baseDoughBalls, + doughBallSizeG: recipe.baseDoughBallSizeG, + }; + } + const values = state.sliderValues[recipe.id]; + + panel.innerHTML = ` +
    +

    ${escapeHtml(recipe.name)}

    +

    ${escapeHtml(recipe.description)}

    +
    + Difficulty: ${escapeHtml(recipe.difficulty)} + Total time: ${recipe.totalTimeHours}h +
    + +
    +
    + + +
    +
    + + +
    +
    + + +
    +
    + +
    +

    Ingredients

    +
      +
      + +
      +

      Loading instructions…

      +
      +
      + `; + + ["slider-hydration", "slider-balls", "slider-size"].forEach((id) => { + panel.querySelector(`#${id}`).addEventListener("input", () => onSliderChange(recipe)); + }); + + renderIngredients(recipe); + + try { + const html = await getInstructionsHtml(recipe); + const instructionsEl = document.getElementById("instructions"); + if (instructionsEl) instructionsEl.innerHTML = html; + } catch (err) { + const instructionsEl = document.getElementById("instructions"); + if (instructionsEl) instructionsEl.innerHTML = `

      ${escapeHtml(err.message)}

      `; + } +} + +function selectRecipe(id) { + state.selectedId = id; + renderPicker(); + const recipe = state.recipes.find((r) => r.id === id); + renderRecipePanel(recipe); +} + +async function init() { + try { + const res = await fetch(RECIPES_URL); + if (!res.ok) throw new Error(`Failed to load ${RECIPES_URL}`); + state.recipes = await res.json(); + if (state.recipes.length === 0) { + document.getElementById("recipe-panel").innerHTML = '

      No recipes found.

      '; + return; + } + selectRecipe(state.recipes[0].id); + } catch (err) { + document.getElementById("recipe-panel").innerHTML = `

      ${escapeHtml(err.message)} — this app must be served over http(s), not opened directly as a file.

      `; + } +} + +init(); diff --git a/index.html b/index.html new file mode 100644 index 0000000..a203b6a --- /dev/null +++ b/index.html @@ -0,0 +1,20 @@ + + + + + + Pizza Recipes + + + + +
      + +

      Loading recipes…

      +
      + + + diff --git a/pizza_recipes.yaml b/pizza_recipes.yaml new file mode 100644 index 0000000..9995bb7 --- /dev/null +++ b/pizza_recipes.yaml @@ -0,0 +1,295 @@ +recipes: + - id: "neapolitan_pizza_dough" + name: "Neapolitan Pizza Dough" + description: "Traditional Italian thin crust pizza dough with long fermentation for maximum flavor" + difficulty: "Medium" + total_time_hours: 24 + image_url: null + variables: + - name: "rise_time_hours" + display: "Rise Time (hours)" + default: 24 + min: 6 + max: 72 + type: "integer" + unit: "hours" + - name: "dough_balls" + display: "Number of Dough Balls" + default: 4 + min: 1 + max: 12 + type: "integer" + unit: "pieces" + - name: "dough_ball_size_g" + display: "Dough Ball Size (g)" + default: 250 + min: 150 + max: 400 + type: "integer" + unit: "g" + - name: "ambient_temp_c" + display: "Room Temperature (°C)" + default: 22 + min: 18 + max: 30 + type: "integer" + unit: "°C" + - name: "yeast_amount_g" + display: "Yeast Amount (g)" + default: 2 + min: 1 + max: 5 + type: "decimal" + unit: "g" + ingredients: + - name: "Bread Flour" + amount: 1000 + unit: "g" + category: "dry" + - name: "Water" + amount: 650 + unit: "ml" + category: "liquid" + - name: "Salt" + amount: 20 + unit: "g" + category: "seasoning" + - name: "Fresh Yeast" + amount: 2 + unit: "g" + category: "leavening" + steps: + - id: "mix_ingredients" + name: "Mix ingredients" + description: "Combine 1kg flour, 650ml water, 20g salt, and {yeast_amount_g}g fresh yeast in a large bowl" + duration_minutes: 10 + timing: "start" + temperature: "Room temperature" + notes: "Use lukewarm water (around 25°C)" + - id: "knead_dough" + name: "Knead dough" + description: "Knead the dough until smooth and elastic, about 10-15 minutes by hand" + duration_minutes: 15 + timing: "after_previous" + notes: "Dough should be slightly sticky but manageable" + - id: "first_rise" + name: "First rise (bulk fermentation)" + description: "Let dough rise in covered bowl at room temperature" + duration_formula: "rise_time_hours * 0.4 * 60" + timing: "after_previous" + temperature: "{ambient_temp_c}°C" + notes: "Dough should roughly double in size" + - id: "divide_shape" + name: "Divide and shape" + description: "Divide dough into {dough_balls} equal portions and shape into balls" + duration_minutes: 15 + timing: "after_previous" + notes: "Use a kitchen scale for even portions" + - id: "final_rise" + name: "Final rise" + description: "Let shaped dough balls rise until ready to use" + duration_formula: "rise_time_hours * 0.6 * 60" + timing: "after_previous" + temperature: "{ambient_temp_c}°C" + notes: "Dough is ready when it springs back slowly when poked" + + - id: "new_york_pizza_dough" + name: "New York Style Pizza Dough" + description: "Classic New York pizza dough with a chewy texture and crispy bottom" + difficulty: "Easy" + total_time_hours: 48 + image_url: null + variables: + - name: "rise_time_hours" + display: "Rise Time (hours)" + default: 48 + min: 24 + max: 96 + type: "integer" + unit: "hours" + - name: "dough_balls" + display: "Number of Dough Balls" + default: 3 + min: 1 + max: 8 + type: "integer" + unit: "pieces" + - name: "dough_ball_size_g" + display: "Dough Ball Size (g)" + default: 250 + min: 150 + max: 400 + type: "integer" + unit: "g" + - name: "ambient_temp_c" + display: "Room Temperature (°C)" + default: 20 + min: 18 + max: 25 + type: "integer" + unit: "°C" + - name: "oil_amount_ml" + display: "Olive Oil (ml)" + default: 30 + min: 20 + max: 50 + type: "integer" + unit: "ml" + ingredients: + - name: "Bread Flour" + amount: 500 + unit: "g" + category: "dry" + - name: "Water" + amount: 320 + unit: "ml" + category: "liquid" + - name: "Salt" + amount: 10 + unit: "g" + category: "seasoning" + - name: "Active Dry Yeast" + amount: 3 + unit: "g" + category: "leavening" + - name: "Olive Oil" + amount: 30 + unit: "ml" + category: "fat" + steps: + - id: "mix_dry" + name: "Mix dry ingredients" + description: "Combine 500g bread flour, 10g salt, and 3g active dry yeast" + duration_minutes: 5 + timing: "start" + - id: "add_liquids" + name: "Add liquids" + description: "Add 320ml cool water and {oil_amount_ml}ml olive oil to dry ingredients" + duration_minutes: 5 + timing: "after_previous" + notes: "Water should be around 18-20°C for slow fermentation" + - id: "mix_dough" + name: "Mix dough" + description: "Mix until dough comes together, don't overwork" + duration_minutes: 8 + timing: "after_previous" + notes: "Dough will be slightly rough, this is normal" + - id: "bulk_ferment" + name: "Bulk fermentation" + description: "Let dough rise in oiled container in refrigerator" + duration_formula: "rise_time_hours * 0.7 * 60" + timing: "after_previous" + temperature: "4°C (refrigerator)" + notes: "Cold fermentation develops flavor" + - id: "portion_dough" + name: "Portion dough" + description: "Remove from fridge and divide into {dough_balls} portions" + duration_minutes: 10 + timing: "after_previous" + temperature: "Room temperature" + - id: "final_proof" + name: "Final proofing" + description: "Let portions come to room temperature and final proof" + duration_formula: "rise_time_hours * 0.3 * 60" + timing: "after_previous" + temperature: "{ambient_temp_c}°C" + notes: "Dough is ready when it feels soft and pliable" + + - id: "sicilian_pizza_dough" + name: "Sicilian Pizza Dough" + description: "Thick, airy Sicilian-style pizza dough perfect for deep dish pans" + difficulty: "Hard" + total_time_hours: 72 + image_url: null + variables: + - name: "rise_time_hours" + display: "Rise Time (hours)" + default: 72 + min: 48 + max: 120 + type: "integer" + unit: "hours" + - name: "dough_balls" + display: "Number of Dough Balls" + default: 1 + min: 1 + max: 4 + type: "integer" + unit: "pieces" + - name: "dough_ball_size_g" + display: "Dough Ball Size (g)" + default: 250 + min: 150 + max: 400 + type: "integer" + unit: "g" + - name: "pan_size_cm" + display: "Pan Size (cm)" + default: 30 + min: 25 + max: 40 + type: "integer" + unit: "cm" + - name: "ambient_temp_c" + display: "Room Temperature (°C)" + default: 21 + min: 18 + max: 24 + type: "integer" + unit: "°C" + - name: "hydration_percent" + display: "Hydration (%)" + default: 75 + min: 70 + max: 80 + type: "integer" + unit: "%" + ingredients: + - name: "Bread Flour" + amount: 600 + unit: "g" + category: "dry" + - name: "Water" + amount: 450 + unit: "ml" + category: "liquid" + - name: "Salt" + amount: 12 + unit: "g" + category: "seasoning" + - name: "Active Dry Yeast" + amount: 1 + unit: "g" + category: "leavening" + steps: + - id: "autolyse" + name: "Autolyse" + description: "Mix 600g bread flour with calculated water amount, let rest" + duration_minutes: 30 + timing: "start" + notes: "Water amount: 600g × {hydration_percent}/100" + - id: "add_salt_yeast" + name: "Add salt and yeast" + description: "Add 12g salt and 1g active dry yeast to autolyse" + duration_minutes: 5 + timing: "after_previous" + - id: "mix_develop" + name: "Mix and develop" + description: "Mix thoroughly and perform stretch and folds" + duration_minutes: 20 + timing: "after_previous" + notes: "Perform 4 sets of stretch and folds, 5 minutes apart" + - id: "bulk_fermentation" + name: "Bulk fermentation" + description: "First rise in oiled container" + duration_formula: "rise_time_hours * 0.5 * 60" + timing: "after_previous" + temperature: "{ambient_temp_c}°C" + notes: "Dough should increase by 50-70%" + - id: "pan_proof" + name: "Pan proofing" + description: "Transfer to oiled {pan_size_cm}cm pan and spread gently" + duration_formula: "rise_time_hours * 0.5 * 60" + timing: "after_previous" + temperature: "{ambient_temp_c}°C" + notes: "Don't force the dough, let it relax and spread naturally" diff --git a/recipes.json b/recipes.json new file mode 100644 index 0000000..9231304 --- /dev/null +++ b/recipes.json @@ -0,0 +1,80 @@ +[ + { + "id": "neapolitan_pizza_dough", + "name": "Neapolitan Pizza Dough", + "description": "Traditional Italian thin crust pizza dough with long fermentation for maximum flavor", + "difficulty": "Medium", + "totalTimeHours": 24, + "baseDoughBalls": 4, + "baseDoughBallSizeG": 250, + "doughBallsRange": { "min": 1, "max": 12 }, + "doughBallSizeRange": { "min": 150, "max": 400 }, + "hydrationRange": { "min": 55, "max": 80 }, + "ingredients": [ + { "name": "Bread Flour", "baseAmount": 1000, "unit": "g", "role": "flour" }, + { "name": "Water", "baseAmount": 650, "unit": "ml", "role": "water" }, + { "name": "Salt", "baseAmount": 20, "unit": "g", "role": "fixed" }, + { "name": "Fresh Yeast", "baseAmount": 2, "unit": "g", "role": "fixed" } + ], + "instructionsFile": "recipes/neapolitan_pizza_dough.md" + }, + { + "id": "new_york_pizza_dough", + "name": "New York Style Pizza Dough", + "description": "Classic New York pizza dough with a chewy texture and crispy bottom", + "difficulty": "Easy", + "totalTimeHours": 48, + "baseDoughBalls": 3, + "baseDoughBallSizeG": 250, + "doughBallsRange": { "min": 1, "max": 8 }, + "doughBallSizeRange": { "min": 150, "max": 400 }, + "hydrationRange": { "min": 55, "max": 80 }, + "ingredients": [ + { "name": "Bread Flour", "baseAmount": 500, "unit": "g", "role": "flour" }, + { "name": "Water", "baseAmount": 320, "unit": "ml", "role": "water" }, + { "name": "Salt", "baseAmount": 10, "unit": "g", "role": "fixed" }, + { "name": "Active Dry Yeast", "baseAmount": 3, "unit": "g", "role": "fixed" }, + { "name": "Olive Oil", "baseAmount": 30, "unit": "ml", "role": "fixed" } + ], + "instructionsFile": "recipes/new_york_pizza_dough.md" + }, + { + "id": "sicilian_pizza_dough", + "name": "Sicilian Pizza Dough", + "description": "Thick, airy Sicilian-style pizza dough perfect for deep dish pans", + "difficulty": "Hard", + "totalTimeHours": 72, + "baseDoughBalls": 1, + "baseDoughBallSizeG": 250, + "doughBallsRange": { "min": 1, "max": 4 }, + "doughBallSizeRange": { "min": 150, "max": 400 }, + "hydrationRange": { "min": 65, "max": 85 }, + "ingredients": [ + { "name": "Bread Flour", "baseAmount": 600, "unit": "g", "role": "flour" }, + { "name": "Water", "baseAmount": 450, "unit": "ml", "role": "water" }, + { "name": "Salt", "baseAmount": 12, "unit": "g", "role": "fixed" }, + { "name": "Active Dry Yeast", "baseAmount": 1, "unit": "g", "role": "fixed" } + ], + "instructionsFile": "recipes/sicilian_pizza_dough.md" + }, + { + "id": "biga_pizza_dough", + "name": "100% Biga Pizza Dough", + "description": "Two-day preferment method using a stiff biga starter for exceptional flavor and an airy, digestible crumb", + "difficulty": "Hard", + "totalTimeHours": 24, + "baseDoughBalls": 6, + "baseDoughBallSizeG": 280, + "doughBallsRange": { "min": 1, "max": 12 }, + "doughBallSizeRange": { "min": 150, "max": 400 }, + "hydrationRange": { "min": 55, "max": 75 }, + "ingredients": [ + { "name": "Bread Flour", "baseAmount": 1000, "unit": "g", "role": "flour" }, + { "name": "Water", "baseAmount": 660, "unit": "ml", "role": "water" }, + { "name": "Salt", "baseAmount": 25, "unit": "g", "role": "fixed" }, + { "name": "Dry Yeast", "baseAmount": 10, "unit": "g", "role": "fixed" }, + { "name": "Malt Extract", "baseAmount": 10, "unit": "g", "role": "fixed" } + ], + "instructionsFile": "recipes/biga_pizza_dough.md" + } +] diff --git a/recipes/biga_pizza_dough.md b/recipes/biga_pizza_dough.md new file mode 100644 index 0000000..86ffb03 --- /dev/null +++ b/recipes/biga_pizza_dough.md @@ -0,0 +1,14 @@ +## Instructions + +**Day 1 — Make the biga** + +1. **Mix the biga** (10 min) — Combine the flour, 450g of the water, and 10g dry yeast into a dry, lumpy mixture. It shouldn't come together into a smooth dough — just no dry flour left. +2. **Rest the biga** (~17 hours) — Cover and let rest at 16-18°C for 16-18 hours, until puffy and full of bubbles. + +**Day 2 — Make the final dough** + +3. **Mix biga with malt and water** (5 min) — Break up the rested biga and mix with 10g malt extract and 150g of the remaining water on low speed. +4. **Add salt and remaining water** (5 min) — Add the salt, then mix on high speed for 4-5 minutes while gradually adding the last 60g water. Target a dough temperature of 23-24°C. +5. **Fold and rest** (20 min) — Fold the dough over itself a few times, then let it rest for 20 minutes. +6. **Divide and ball** (15 min) — Divide into 6 portions of 280g each and shape into balls. +7. **Proof** (1-2 hours) — Let the balls rest at room temperature for 1-2 hours before baking. Alternatively, rest 1 hour and refrigerate until needed. diff --git a/recipes/neapolitan_pizza_dough.md b/recipes/neapolitan_pizza_dough.md new file mode 100644 index 0000000..7b67cf1 --- /dev/null +++ b/recipes/neapolitan_pizza_dough.md @@ -0,0 +1,7 @@ +## Instructions + +1. **Mix ingredients** (10 min) — Combine the flour, water, salt, and 2g fresh yeast in a large bowl. Use lukewarm water, around 25°C. +2. **Knead dough** (15 min) — Knead until smooth and elastic, about 10-15 minutes by hand. Dough should be slightly sticky but manageable. +3. **First rise** (~9.6 hours) — Let dough rise in a covered bowl at 22°C. Dough should roughly double in size. +4. **Divide and shape** (15 min) — Divide dough into 4 equal portions and shape into balls. Use a kitchen scale for even portions. +5. **Final rise** (~14.4 hours) — Let shaped dough balls rise at 22°C until ready to use. Dough is ready when it springs back slowly when poked. diff --git a/recipes/new_york_pizza_dough.md b/recipes/new_york_pizza_dough.md new file mode 100644 index 0000000..1b4b99d --- /dev/null +++ b/recipes/new_york_pizza_dough.md @@ -0,0 +1,8 @@ +## Instructions + +1. **Mix dry ingredients** (5 min) — Combine the flour, salt, and active dry yeast. +2. **Add liquids** (5 min) — Add the water and 30ml olive oil to the dry ingredients. Water should be around 18-20°C for slow fermentation. +3. **Mix dough** (8 min) — Mix until dough comes together, don't overwork. Dough will be slightly rough, this is normal. +4. **Bulk fermentation** (~33.6 hours) — Let dough rise in an oiled container in the refrigerator at 4°C. Cold fermentation develops flavor. +5. **Portion dough** (10 min) — Remove from fridge and divide into 3 portions at room temperature. +6. **Final proofing** (~14.4 hours) — Let portions come to room temperature (20°C) and final proof. Dough is ready when it feels soft and pliable. diff --git a/recipes/sicilian_pizza_dough.md b/recipes/sicilian_pizza_dough.md new file mode 100644 index 0000000..c88c117 --- /dev/null +++ b/recipes/sicilian_pizza_dough.md @@ -0,0 +1,7 @@ +## Instructions + +1. **Autolyse** (30 min) — Mix the flour with the water, let rest. At 75% hydration that's 600g flour to 450g water. +2. **Add salt and yeast** (5 min) — Add the salt and active dry yeast to the autolyse. +3. **Mix and develop** (20 min) — Mix thoroughly and perform 4 sets of stretch and folds, 5 minutes apart. +4. **Bulk fermentation** (~36 hours) — First rise in an oiled container at 21°C. Dough should increase by 50-70%. +5. **Pan proofing** (~36 hours) — Transfer to an oiled 30cm pan and spread gently at 21°C. Don't force the dough, let it relax and spread naturally. diff --git a/style.css b/style.css new file mode 100644 index 0000000..b851d67 --- /dev/null +++ b/style.css @@ -0,0 +1,175 @@ +:root { + --bg: #faf6f0; + --panel: #ffffff; + --border: #e5ddd0; + --text: #2b2320; + --muted: #7a6f63; + --accent: #c0392b; + --accent-light: #f5e3e0; +} + +* { box-sizing: border-box; } + +body { + margin: 0; + font-family: -apple-system, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; + background: var(--bg); + color: var(--text); +} + +header.site-header { + padding: 2rem 1.5rem 1rem; + text-align: center; +} + +header.site-header h1 { + margin: 0 0 0.25rem; + font-size: 1.8rem; +} + +header.site-header p { + margin: 0; + color: var(--muted); +} + +main { + max-width: 900px; + margin: 0 auto; + padding: 0 1.5rem 3rem; +} + +.recipe-picker { + display: flex; + gap: 0.5rem; + flex-wrap: wrap; + justify-content: center; + margin: 1.5rem 0; +} + +.recipe-picker button { + padding: 0.6rem 1.1rem; + border-radius: 999px; + border: 1px solid var(--border); + background: var(--panel); + color: var(--text); + cursor: pointer; + font-size: 0.95rem; +} + +.recipe-picker button:hover { + border-color: var(--accent); +} + +.recipe-picker button.active { + background: var(--accent); + border-color: var(--accent); + color: #fff; +} + +.recipe-panel { + background: var(--panel); + border: 1px solid var(--border); + border-radius: 12px; + padding: 1.75rem; +} + +.recipe-panel h2 { + margin: 0 0 0.4rem; +} + +.recipe-desc { + color: var(--muted); + margin: 0 0 0.75rem; +} + +.recipe-meta { + display: flex; + gap: 1rem; + flex-wrap: wrap; + margin-bottom: 1.5rem; + font-size: 0.9rem; + color: var(--muted); +} + +.recipe-meta span { + background: var(--accent-light); + padding: 0.25rem 0.65rem; + border-radius: 6px; +} + +.sliders { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); + gap: 1.25rem; + margin-bottom: 1.75rem; + padding: 1.25rem; + background: var(--bg); + border-radius: 10px; +} + +.slider-field label { + display: flex; + justify-content: space-between; + font-size: 0.9rem; + margin-bottom: 0.35rem; +} + +.slider-field .value { + font-weight: 600; + color: var(--accent); +} + +.slider-field input[type="range"] { + width: 100%; +} + +.ingredients h3, +.instructions h3 { + margin-bottom: 0.6rem; +} + +.ingredients ul { + list-style: none; + margin: 0 0 1.75rem; + padding: 0; + display: grid; + gap: 0.4rem; +} + +.ingredients li { + display: flex; + justify-content: space-between; + padding: 0.55rem 0.8rem; + background: var(--bg); + border-radius: 8px; + font-size: 0.95rem; +} + +.ingredients li .amount { + font-weight: 600; +} + +.instructions { + border-top: 1px solid var(--border); + padding-top: 1.5rem; +} + +.instructions ol { + padding-left: 1.2rem; +} + +.instructions li { + margin-bottom: 0.6rem; + line-height: 1.5; +} + +.loading, +.error { + text-align: center; + color: var(--muted); + padding: 2rem; +} + +.error { + color: var(--accent); +}