← back to carl65455-ai__recepty

Function bodies 62 total

All specs Real LLM only Function bodies
utc_now function · python · L10-L12 (3 LOC)
src/backend/recepty/database.py
def utc_now() -> str:
    """Return current UTC time as ISO format string."""
    return datetime.now(timezone.utc).isoformat()
get_connection function · python · L15-L19 (5 LOC)
src/backend/recepty/database.py
def get_connection() -> sqlite3.Connection:
    """Get a database connection with Row factory enabled."""
    conn = sqlite3.connect(DB_PATH)
    conn.row_factory = sqlite3.Row
    return conn
init_db function · python · L22-L48 (27 LOC)
src/backend/recepty/database.py
def init_db() -> None:
    """Initialize the database schema and seed if empty."""
    with get_connection() as conn:
        conn.execute(
            """
            CREATE TABLE IF NOT EXISTS recipes (
                id INTEGER PRIMARY KEY AUTOINCREMENT,
                title TEXT NOT NULL,
                ingredients TEXT NOT NULL DEFAULT '',
                instructions TEXT NOT NULL DEFAULT '',
                prep_time_minutes INTEGER,
                image_url TEXT NOT NULL DEFAULT '',
                image_data TEXT NOT NULL DEFAULT '',
                created_at TEXT NOT NULL,
                updated_at TEXT NOT NULL
            )
            """
        )
        columns = {
            row["name"]
            for row in conn.execute("PRAGMA table_info(recipes)").fetchall()
        }
        if "prep_time_minutes" not in columns:
            conn.execute("ALTER TABLE recipes ADD COLUMN prep_time_minutes INTEGER")
        count = conn.execute("SELECT COUNT(*) AS count FROM r
seed_database function · python · L51-L76 (26 LOC)
src/backend/recepty/database.py
def seed_database(conn: sqlite3.Connection) -> None:
    """Seed the database with initial recipes from JSON file."""
    if not SEED_PATH.exists():
        return

    try:
        raw = json.loads(SEED_PATH.read_text(encoding="utf-8"))
    except json.JSONDecodeError:
        return

    recipes = raw if isinstance(raw, list) else raw.get("recipes", [])
    now = utc_now()
    for recipe in recipes:
        conn.execute(
            """
            INSERT INTO recipes (title, ingredients, instructions, prep_time_minutes, image_url, image_data, created_at, updated_at)
            VALUES (?, ?, ?, NULL, '', '', ?, ?)
            """,
            (
                str(recipe.get("title") or "Bez nazvu"),
                str(recipe.get("ingredients") or ""),
                str(recipe.get("instructions") or ""),
                now,
                now,
            ),
        )
RecipeHandler class · python · L14-L275 (262 LOC)
src/backend/recepty/handlers.py
class RecipeHandler(BaseHTTPRequestHandler):
    """HTTP request handler for recipe operations and static file serving."""

    server_version = "Receptar/1.0"

    def do_GET(self) -> None:
        """Handle GET requests for listing, viewing, and printing recipes."""
        parsed = urlparse(self.path)
        path = parsed.path

        if path == "/api/recipes":
            self.handle_list_recipes()
            return

        if path.startswith("/print/"):
            self.handle_print_recipe(path)
            return

        if path.startswith("/api/recipes/"):
            self.handle_get_recipe(path)
            return

        self.serve_static(path)

    def do_POST(self) -> None:
        """Handle POST requests for creating recipes."""
        if self.path == "/api/recipes":
            self.handle_create_recipe()
            return
        self.send_json({"error": "Endpoint neexistuje."}, status=404)

    def do_PUT(self) -> None:
        """Handle PUT requests for updating
do_GET method · python · L19-L36 (18 LOC)
src/backend/recepty/handlers.py
    def do_GET(self) -> None:
        """Handle GET requests for listing, viewing, and printing recipes."""
        parsed = urlparse(self.path)
        path = parsed.path

        if path == "/api/recipes":
            self.handle_list_recipes()
            return

        if path.startswith("/print/"):
            self.handle_print_recipe(path)
            return

        if path.startswith("/api/recipes/"):
            self.handle_get_recipe(path)
            return

        self.serve_static(path)
do_POST method · python · L38-L43 (6 LOC)
src/backend/recepty/handlers.py
    def do_POST(self) -> None:
        """Handle POST requests for creating recipes."""
        if self.path == "/api/recipes":
            self.handle_create_recipe()
            return
        self.send_json({"error": "Endpoint neexistuje."}, status=404)
Repobility · open methodology · https://repobility.com/research/
do_PUT method · python · L45-L50 (6 LOC)
src/backend/recepty/handlers.py
    def do_PUT(self) -> None:
        """Handle PUT requests for updating recipes."""
        if self.path.startswith("/api/recipes/"):
            self.handle_update_recipe(self.path)
            return
        self.send_json({"error": "Endpoint neexistuje."}, status=404)
do_DELETE method · python · L52-L57 (6 LOC)
src/backend/recepty/handlers.py
    def do_DELETE(self) -> None:
        """Handle DELETE requests for removing recipes."""
        if self.path.startswith("/api/recipes/"):
            self.handle_delete_recipe(self.path)
            return
        self.send_json({"error": "Endpoint neexistuje."}, status=404)
handle_list_recipes method · python · L59-L69 (11 LOC)
src/backend/recepty/handlers.py
    def handle_list_recipes(self) -> None:
        """Return all recipes sorted by title."""
        with get_connection() as conn:
            rows = conn.execute(
                """
                SELECT id, title, ingredients, instructions, prep_time_minutes, image_url, image_data, created_at, updated_at
                FROM recipes
                ORDER BY title COLLATE NOCASE ASC, id ASC
                """
            ).fetchall()
        self.send_json({"recipes": [row_to_recipe(row) for row in rows]})
handle_get_recipe method · python · L71-L90 (20 LOC)
src/backend/recepty/handlers.py
    def handle_get_recipe(self, path: str) -> None:
        """Return a single recipe by ID."""
        recipe_id = self.parse_recipe_id(path)
        if recipe_id is None:
            self.send_json({"error": "Neplatne ID receptu."}, status=400)
            return

        with get_connection() as conn:
            row = conn.execute(
                """
                SELECT id, title, ingredients, instructions, prep_time_minutes, image_url, image_data, created_at, updated_at
                FROM recipes
                WHERE id = ?
                """,
                (recipe_id,),
            ).fetchone()
        if row is None:
            self.send_json({"error": "Recept sa nenasiel."}, status=404)
            return
        self.send_json({"recipe": row_to_recipe(row)})
handle_print_recipe method · python · L92-L119 (28 LOC)
src/backend/recepty/handlers.py
    def handle_print_recipe(self, path: str) -> None:
        """Return a printable HTML page for a recipe."""
        recipe_id = self.parse_recipe_id(path)
        if recipe_id is None:
            self.send_error(400)
            return

        with get_connection() as conn:
            row = conn.execute(
                """
                SELECT id, title, ingredients, instructions, prep_time_minutes, image_url, image_data, created_at, updated_at
                FROM recipes
                WHERE id = ?
                """,
                (recipe_id,),
            ).fetchone()

        if row is None:
            self.send_error(404)
            return

        recipe = row_to_recipe(row)
        content = render_print_html(recipe).encode("utf-8")
        self.send_response(200)
        self.send_header("Content-Type", "text/html; charset=utf-8")
        self.send_header("Content-Length", str(len(content)))
        self.end_headers()
        self.wfile.write(content)
handle_create_recipe method · python · L121-L156 (36 LOC)
src/backend/recepty/handlers.py
    def handle_create_recipe(self) -> None:
        """Create a new recipe from JSON payload."""
        payload = self.read_json_body()
        if payload is None:
            return

        title = str(payload.get("title") or "").strip()
        if not title:
            self.send_json({"error": "Nazov receptu je povinny."}, status=400)
            return

        ingredients = str(payload.get("ingredients") or "").strip()
        instructions = str(payload.get("instructions") or "").strip()
        prep_time_minutes = parse_prep_time(payload.get("prep_time_minutes"))
        image_url = str(payload.get("image_url") or "").strip()
        image_data = str(payload.get("image_data") or "").strip()
        now = utc_now()

        with get_connection() as conn:
            cursor = conn.execute(
                """
                INSERT INTO recipes (title, ingredients, instructions, prep_time_minutes, image_url, image_data, created_at, updated_at)
                VALUES (?, ?, ?, ?, 
handle_update_recipe method · python · L158-L202 (45 LOC)
src/backend/recepty/handlers.py
    def handle_update_recipe(self, path: str) -> None:
        """Update an existing recipe from JSON payload."""
        recipe_id = self.parse_recipe_id(path)
        if recipe_id is None:
            self.send_json({"error": "Neplatne ID receptu."}, status=400)
            return

        payload = self.read_json_body()
        if payload is None:
            return

        title = str(payload.get("title") or "").strip()
        if not title:
            self.send_json({"error": "Nazov receptu je povinny."}, status=400)
            return

        ingredients = str(payload.get("ingredients") or "").strip()
        instructions = str(payload.get("instructions") or "").strip()
        prep_time_minutes = parse_prep_time(payload.get("prep_time_minutes"))
        image_url = str(payload.get("image_url") or "").strip()
        image_data = str(payload.get("image_data") or "").strip()
        now = utc_now()

        with get_connection() as conn:
            cursor = conn.execute(
     
handle_delete_recipe method · python · L204-L216 (13 LOC)
src/backend/recepty/handlers.py
    def handle_delete_recipe(self, path: str) -> None:
        """Delete a recipe by ID."""
        recipe_id = self.parse_recipe_id(path)
        if recipe_id is None:
            self.send_json({"error": "Neplatne ID receptu."}, status=400)
            return

        with get_connection() as conn:
            cursor = conn.execute("DELETE FROM recipes WHERE id = ?", (recipe_id,))
        if cursor.rowcount == 0:
            self.send_json({"error": "Recept sa nenasiel."}, status=404)
            return
        self.send_json({"ok": True})
Same scanner, your repo: https://repobility.com — Repobility
serve_static method · python · L218-L239 (22 LOC)
src/backend/recepty/handlers.py
    def serve_static(self, path: str) -> None:
        """Serve static files from the frontend directory."""
        relative = path.lstrip("/") or "index.html"
        target = (STATIC_DIR / relative).resolve()
        if not str(target).startswith(str(STATIC_DIR)):
            self.send_error(403)
            return

        if target.is_dir():
            target = target / "index.html"

        if not target.exists() or not target.is_file():
            self.send_error(404)
            return

        mime_type, _ = mimetypes.guess_type(str(target))
        content = target.read_bytes()
        self.send_response(200)
        self.send_header("Content-Type", mime_type or "application/octet-stream")
        self.send_header("Content-Length", str(len(content)))
        self.end_headers()
        self.wfile.write(content)
read_json_body method · python · L241-L255 (15 LOC)
src/backend/recepty/handlers.py
    def read_json_body(self) -> dict | None:
        """Read and parse JSON from request body."""
        length_header = self.headers.get("Content-Length", "0")
        try:
            length = int(length_header)
        except ValueError:
            self.send_json({"error": "Neplatna dlzka poziadavky."}, status=400)
            return None

        raw_body = self.rfile.read(length)
        try:
            return json.loads(raw_body or b"{}")
        except json.JSONDecodeError:
            self.send_json({"error": "Telo poziadavky nie je validny JSON."}, status=400)
            return None
parse_recipe_id method · python · L257-L262 (6 LOC)
src/backend/recepty/handlers.py
    def parse_recipe_id(self, path: str) -> int | None:
        """Extract recipe ID from URL path."""
        try:
            return int(path.rsplit("/", 1)[-1])
        except ValueError:
            return None
send_json method · python · L264-L271 (8 LOC)
src/backend/recepty/handlers.py
    def send_json(self, payload: dict, status: int = 200) -> None:
        """Send a JSON response."""
        data = json.dumps(payload, ensure_ascii=False).encode("utf-8")
        self.send_response(status)
        self.send_header("Content-Type", "application/json; charset=utf-8")
        self.send_header("Content-Length", str(len(data)))
        self.end_headers()
        self.wfile.write(data)
log_message method · python · L273-L275 (3 LOC)
src/backend/recepty/handlers.py
    def log_message(self, format: str, *args) -> None:
        """Suppress default HTTP logging."""
        return
main function · python · L11-L23 (13 LOC)
src/backend/recepty/__main__.py
def main() -> None:
    """Start the Recepty server."""
    init_db()
    host = os.environ.get("HOST", DEFAULT_HOST)
    port = int(os.environ.get("PORT", str(DEFAULT_PORT)))
    server = ThreadingHTTPServer((host, port), RecipeHandler)
    print(f"Receptar server running on http://{host}:{port}")
    try:
        server.serve_forever()
    except KeyboardInterrupt:
        pass
    finally:
        server.server_close()
row_to_recipe function · python · L6-L18 (13 LOC)
src/backend/recepty/models.py
def row_to_recipe(row: sqlite3.Row) -> dict:
    """Convert a database row to a recipe dictionary."""
    return {
        "id": row["id"],
        "title": row["title"],
        "ingredients": row["ingredients"],
        "instructions": row["instructions"],
        "prep_time_minutes": row["prep_time_minutes"],
        "image_url": row["image_url"],
        "image_data": row["image_data"],
        "created_at": row["created_at"],
        "updated_at": row["updated_at"],
    }
parse_prep_time function · python · L21-L29 (9 LOC)
src/backend/recepty/models.py
def parse_prep_time(value) -> int | None:
    """Parse preparation time value to integer minutes or None."""
    if value in (None, "", False):
        return None
    try:
        minutes = int(value)
    except (TypeError, ValueError):
        return None
    return minutes if minutes >= 0 else None
Citation: Repobility (2026). State of AI-Generated Code. https://repobility.com/research/
render_print_html function · python · L6-L97 (92 LOC)
src/backend/recepty/templates.py
def render_print_html(recipe: dict) -> str:
    """Render a recipe as a printable HTML page."""
    title = escape_html(recipe.get("title") or "Bez nazvu")
    prep_time = format_prep_time(recipe.get("prep_time_minutes"))
    ingredients = ingredients_to_html(recipe.get("ingredients"))
    instructions = lines_to_html_list(recipe.get("instructions"), ordered=True)

    return f"""<!doctype html>
<html lang="sk">
<head>
  <meta charset="UTF-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
  <title>{title}</title>
  <style>
    @page {{ margin: 14mm; }}
    * {{ box-sizing: border-box; }}
    body {{ font-family: Georgia, serif; color: #111; margin: 0; background: #f4f4f4; }}
    .print-shell {{ max-width: 900px; margin: 0 auto; padding: 24px; }}
    .print-toolbar {{
      position: sticky;
      top: 16px;
      z-index: 10;
      display: flex;
      justify-content: space-between;
      gap: 12px;
      margin-bottom: 18px;
      padding: 14px;
      b
escape_html function · python · L100-L102 (3 LOC)
src/backend/recepty/templates.py
def escape_html(value: str) -> str:
    """Escape HTML special characters."""
    return html.escape(str(value), quote=True)
format_prep_time function · python · L105-L113 (9 LOC)
src/backend/recepty/templates.py
def format_prep_time(value) -> str:
    """Format preparation time for display."""
    if value in (None, ""):
        return "- min"
    try:
        minutes = int(value)
    except (TypeError, ValueError):
        return "- min"
    return f"{minutes} min" if minutes >= 0 else "- min"
lines_to_html_list function · python · L116-L125 (10 LOC)
src/backend/recepty/templates.py
def lines_to_html_list(value, ordered: bool) -> str:
    """Convert newline-separated text to HTML list."""
    items = [line.strip() for line in str(value or "").splitlines() if line.strip()]
    if not items:
        fallback = "<li>Bez postupu</li>" if ordered else "<li>Bez surovin</li>"
        tag = "ol" if ordered else "ul"
        return f"<{tag}>{fallback}</{tag}>"
    tag = "ol" if ordered else "ul"
    rendered = "".join(f"<li>{escape_html(item)}</li>" for item in items)
    return f"<{tag}>{rendered}</{tag}>"
ingredients_to_html function · python · L128-L147 (20 LOC)
src/backend/recepty/templates.py
def ingredients_to_html(value) -> str:
    """Convert ingredients text to HTML with support for grouped sections."""
    groups = parse_ingredient_groups(value)
    if not groups:
        return "<ul><li>Bez surovin</li></ul>"

    has_sections = any(group["title"] for group in groups)
    if not has_sections:
        rendered = "".join(f"<li>{escape_html(item)}</li>" for item in groups[0]["items"])
        return f"<ul>{rendered}</ul>"

    blocks = []
    for group in groups:
        title_html = f'<p class="group-title">{escape_html(group["title"])}</p>' if group["title"] else ""
        list_html = ""
        if group["items"]:
            rendered = "".join(f"<li>{escape_html(item)}</li>" for item in group["items"])
            list_html = f"<ul>{rendered}</ul>"
        blocks.append(f"<section>{title_html}{list_html}</section>")
    return f'<div class="ingredient-groups">{"".join(blocks)}</div>'
parse_ingredient_groups function · python · L150-L166 (17 LOC)
src/backend/recepty/templates.py
def parse_ingredient_groups(value) -> list[dict]:
    """Parse ingredients text into groups based on section headers (lines ending with ':')."""
    lines = [line.strip() for line in str(value or "").splitlines() if line.strip()]
    groups: list[dict] = []
    current = {"title": "", "items": []}

    for line in lines:
      if line.endswith(":"):
        if current["title"] or current["items"]:
            groups.append(current)
        current = {"title": line, "items": []}
        continue
      current["items"].append(line)

    if current["title"] or current["items"]:
        groups.append(current)
    return groups
loadRecipes function · javascript · L3-L21 (19 LOC)
src/frontend/js/api.js
async function loadRecipes() {
  statusEl.textContent = "Nacitavam recepty z databazy...";
  try {
    const response = await fetch("/api/recipes");
    if (!response.ok) throw new Error("Nepodarilo sa nacitat recepty.");
    const payload = await response.json();
    state.recipes = Array.isArray(payload.recipes) ? payload.recipes : [];
    heroCountEl.textContent = String(state.recipes.length);
    statusEl.textContent = "Databaza je pripravena. Recepty mozes aj upravovat.";
    sortSelectEl.value = state.sort;
    applyFilters();
  } catch (error) {
    state.recipes = [];
    state.filtered = [];
    statusEl.textContent = error.message;
    renderList();
    renderDetail();
  }
}
saveRecipe function · javascript · L23-L65 (43 LOC)
src/frontend/js/api.js
async function saveRecipe(event) {
  event.preventDefault();
  formMessageEl.textContent = "";
  formMessageEl.classList.remove("error");

  const payload = {
    title: titleInputEl.value.trim(),
    ingredients: ingredientsInputEl.value.trim(),
    instructions: instructionsInputEl.value.trim(),
    prep_time_minutes: normalizePrepTimeInput(prepTimeInputEl.value),
    image_url: imageUrlInputEl.value.trim(),
    image_data: state.draftImageData || ""
  };

  if (!payload.title) {
    formMessageEl.textContent = "Nazov receptu je povinny.";
    formMessageEl.classList.add("error");
    return;
  }

  const method = state.editingId ? "PUT" : "POST";
  const endpoint = state.editingId ? `/api/recipes/${state.editingId}` : "/api/recipes";

  try {
    const response = await fetch(endpoint, {
      method,
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify(payload)
    });
    const result = await response.json();
    if (!response.ok) {
      throw new Erro
Want fix-PRs on findings? Install Repobility's GitHub App · github.com/apps/repobility-bot
deleteRecipe function · javascript · L67-L70 (4 LOC)
src/frontend/js/api.js
async function deleteRecipe() {
  if (!state.editingId) return;
  await deleteRecipeById(state.editingId, true);
}
deleteRecipeById function · javascript · L72-L91 (20 LOC)
src/frontend/js/api.js
async function deleteRecipeById(recipeId, closeEditModal = false) {
  const confirmed = window.confirm("Naozaj chces vymazat tento recept?");
  if (!confirmed) return;
  try {
    const response = await fetch(`/api/recipes/${recipeId}`, { method: "DELETE" });
    const result = await response.json();
    if (!response.ok) {
      throw new Error(result.error || "Nepodarilo sa vymazat recept.");
    }
    if (closeEditModal) closeForm();
    await loadRecipes();
  } catch (error) {
    if (closeEditModal) {
      formMessageEl.textContent = error.message;
      formMessageEl.classList.add("error");
    } else {
      alert(error.message);
    }
  }
}
init function · javascript · L3-L6 (4 LOC)
src/frontend/js/app.js
async function init() {
  bindEvents();
  await loadRecipes();
}
bindEvents function · javascript · L8-L71 (64 LOC)
src/frontend/js/app.js
function bindEvents() {
  searchInputEl.addEventListener("input", () => {
    state.query = searchInputEl.value.trim().toLowerCase();
    applyFilters();
  });

  sortSelectEl.addEventListener("change", () => {
    state.sort = sortSelectEl.value;
    applyFilters();
  });

  resetFiltersButtonEl.addEventListener("click", resetFilters);

  addRecipeButtonEl.addEventListener("click", () => openForm());
  editRecipeButtonEl.addEventListener("click", () => {
    const recipe = getSelectedRecipe();
    if (recipe) openForm(recipe);
  });
  deleteRecipeToolbarButtonEl.addEventListener("click", () => {
    const recipe = getSelectedRecipe();
    if (recipe) deleteRecipeById(recipe.id);
  });
  printRecipeButtonEl.addEventListener("click", printSelectedRecipe);
  mobileListToggleEl.addEventListener("click", () => {
    sidebarEl.classList.toggle("open");
    mobileListToggleEl.textContent = sidebarEl.classList.contains("open") ? "Zavriet zoznam" : "Zoznam receptov";
  });

  closeFormButtonEl
applyFilters function · javascript · L73-L97 (25 LOC)
src/frontend/js/app.js
function applyFilters() {
  const query = state.query;
  const recipes = [...state.recipes].filter((recipe) => {
    if (!query) return true;
    const haystack = [recipe.title, recipe.ingredients, recipe.instructions]
      .join("\n")
      .toLowerCase();
    return haystack.includes(query);
  });

  recipes.sort((left, right) => {
    if (state.sort === "title-asc") {
      return left.title.localeCompare(right.title, "sk");
    }
    if (state.sort === "title-desc") {
      return right.title.localeCompare(left.title, "sk");
    }
    return String(right.updated_at || "").localeCompare(String(left.updated_at || ""));
  });

  state.filtered = recipes;
  ensureSelection();
  renderList();
  renderDetail();
}
ensureSelection function · javascript · L99-L109 (11 LOC)
src/frontend/js/app.js
function ensureSelection() {
  if (!state.filtered.length) {
    state.selectedId = null;
    return;
  }

  const exists = state.filtered.some((recipe) => recipe.id === state.selectedId);
  if (!exists) {
    state.selectedId = state.filtered[0].id;
  }
}
resetFilters function · javascript · L111-L117 (7 LOC)
src/frontend/js/app.js
function resetFilters() {
  state.query = "";
  state.sort = "title-asc";
  searchInputEl.value = "";
  sortSelectEl.value = state.sort;
  applyFilters();
}
openForm function · javascript · L119-L149 (31 LOC)
src/frontend/js/app.js
function openForm(recipe) {
  state.isFormOpen = true;
  state.editingId = recipe ? recipe.id : null;
  state.draftImageData = recipe ? (recipe.image_data || "") : "";
  overlayEl.classList.add("open");
  document.body.style.overflow = "hidden";
  formModalEl.scrollTop = 0;
  formMessageEl.textContent = "";
  formMessageEl.classList.remove("error");

  if (recipe) {
    formTitleEl.textContent = "Upravit recept";
    formSubtitleEl.textContent = "Zmen recept, obrazok alebo postup. Ulozi sa priamo do databazy.";
    deleteRecipeButtonEl.hidden = false;
    openCsvImportButtonEl.hidden = true;
  } else {
    formTitleEl.textContent = "Novy recept";
    formSubtitleEl.textContent = "Pridaj recept rucne. Ulozi sa do lokalnej databazy.";
    deleteRecipeButtonEl.hidden = true;
    openCsvImportButtonEl.hidden = false;
  }

  titleInputEl.value = recipe ? recipe.title : "";
  imageUrlInputEl.value = recipe ? (recipe.image_url || "") : "";
  prepTimeInputEl.value = recipe && recipe.prep_time_
Repobility · open methodology · https://repobility.com/research/
closeForm function · javascript · L151-L160 (10 LOC)
src/frontend/js/app.js
function closeForm() {
  state.isFormOpen = false;
  state.editingId = null;
  state.draftImageData = "";
  overlayEl.classList.remove("open");
  document.body.style.overflow = "";
  recipeFormEl.reset();
  formModalEl.scrollTop = 0;
  imagePreviewEl.src = FALLBACK_IMAGE;
}
openCsvImport function · javascript · L162-L166 (5 LOC)
src/frontend/js/app.js
function openCsvImport() {
  csvOverlayEl.classList.add("open");
  csvMessageEl.textContent = "";
  csvMessageEl.classList.remove("error");
}
closeCsvImport function · javascript · L168-L173 (6 LOC)
src/frontend/js/app.js
function closeCsvImport() {
  csvOverlayEl.classList.remove("open");
  csvImportInputEl.value = "";
  csvMessageEl.textContent = "";
  csvMessageEl.classList.remove("error");
}
printSelectedRecipe function · javascript · L175-L179 (5 LOC)
src/frontend/js/app.js
function printSelectedRecipe() {
  const recipe = getSelectedRecipe();
  if (!recipe) return;
  window.print();
}
getSelectedRecipe function · javascript · L181-L183 (3 LOC)
src/frontend/js/app.js
function getSelectedRecipe() {
  return state.recipes.find((recipe) => recipe.id === state.selectedId) || null;
}
detectCsvDelimiter function · javascript · L3-L28 (26 LOC)
src/frontend/js/csv.js
function detectCsvDelimiter(text) {
  const firstLine = String(text || "").split(/\r?\n/, 1)[0] || "";
  let inQuotes = false;
  let semicolons = 0;
  let commas = 0;

  for (let index = 0; index < firstLine.length; index += 1) {
    const char = firstLine[index];
    const next = firstLine[index + 1];

    if (char === '"') {
      if (inQuotes && next === '"') {
        index += 1;
      } else {
        inQuotes = !inQuotes;
      }
      continue;
    }

    if (inQuotes) continue;
    if (char === ";") semicolons += 1;
    if (char === ",") commas += 1;
  }

  return semicolons >= commas ? ";" : ",";
}
parseCsvText function · javascript · L30-L74 (45 LOC)
src/frontend/js/csv.js
function parseCsvText(text) {
  const rows = [];
  let row = [];
  let value = "";
  let inQuotes = false;
  const delimiter = detectCsvDelimiter(text);

  for (let index = 0; index < text.length; index += 1) {
    const char = text[index];
    const next = text[index + 1];

    if (char === '"') {
      if (inQuotes && next === '"') {
        value += '"';
        index += 1;
      } else {
        inQuotes = !inQuotes;
      }
      continue;
    }

    if (!inQuotes && char === delimiter) {
      row.push(value.trim());
      value = "";
      continue;
    }

    if (!inQuotes && (char === "\n" || char === "\r")) {
      if (char === "\r" && next === "\n") index += 1;
      row.push(value.trim());
      value = "";
      if (row.some((cell) => cell !== "")) rows.push(row);
      row = [];
      continue;
    }

    value += char;
  }

  if (value.length || row.length) {
    row.push(value.trim());
  }
  if (row.some((cell) => cell !== "")) rows.push(row);
  return rows;
}
decodeCsvCellNewlines function · javascript · L76-L80 (5 LOC)
src/frontend/js/csv.js
function decodeCsvCellNewlines(value) {
  return String(value || "")
    .replace(/\\n/g, "\n")
    .trim();
}
Same scanner, your repo: https://repobility.com — Repobility
mapCsvRowsToRecipes function · javascript · L82-L102 (21 LOC)
src/frontend/js/csv.js
function mapCsvRowsToRecipes(rows) {
  const expected = ["title", "prep_time_minutes", "ingredients", "instructions", "image_url"];
  let dataRows = rows;

  if (rows.length) {
    const normalizedHeader = rows[0].map((cell) => cell.trim().toLowerCase());
    const looksLikeHeader = expected.every((name, index) => normalizedHeader[index] === name);
    if (looksLikeHeader) dataRows = rows.slice(1);
  }

  return dataRows
    .map((row) => ({
      title: String(row[0] || "").trim(),
      prep_time_minutes: normalizePrepTimeInput(row[1]),
      ingredients: decodeCsvCellNewlines(row[2]),
      instructions: decodeCsvCellNewlines(row[3]),
      image_url: String(row[4] || "").trim(),
      image_data: ""
    }))
    .filter((recipe) => recipe.title);
}
importCsvRecipes function · javascript · L104-L149 (46 LOC)
src/frontend/js/csv.js
async function importCsvRecipes() {
  const [file] = csvImportInputEl.files || [];
  if (!file) {
    csvMessageEl.textContent = "Najprv vyber CSV subor.";
    csvMessageEl.classList.add("error");
    return;
  }

  csvMessageEl.textContent = "";
  csvMessageEl.classList.remove("error");

  try {
    const text = await file.text();
    const rows = parseCsvText(text);
    if (!rows.length) {
      throw new Error("CSV subor je prazdny.");
    }

    const recipes = mapCsvRowsToRecipes(rows);
    if (!recipes.length) {
      throw new Error("V CSV sa nenasiel ziadny validny recept.");
    }

    for (const recipe of recipes) {
      const response = await fetch("/api/recipes", {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify(recipe)
      });
      const result = await response.json();
      if (!response.ok) {
        throw new Error(result.error || `Nepodarilo sa importovat recept ${recipe.title}.`);
      }
    }

    awa
renderList function · javascript · L3-L41 (39 LOC)
src/frontend/js/ui.js
function renderList() {
  filteredCountEl.textContent = `${state.filtered.length} ks`;
  if (!state.filtered.length) {
    recipeListEl.innerHTML = '<div class="empty-state">Zatial tu nic nesedi na tento filter.<br />Skus iny vyraz alebo pridaj novy recept.</div>';
    return;
  }

  recipeListEl.innerHTML = state.filtered.map((recipe) => {
    const activeClass = recipe.id === state.selectedId ? " active" : "";
    const ingredientCount = countLines(recipe.ingredients);
    const prepTime = formatPrepTime(recipe.prep_time_minutes);
    return `
      <button class="recipe-card${activeClass}" type="button" data-id="${recipe.id}">
        <div class="recipe-thumb">
          <img src="${escapeHtml(getRecipeImage(recipe))}" alt="${escapeHtml(recipe.title)}" loading="lazy" onerror="this.onerror=null;this.src='${FALLBACK_IMAGE}'" />
        </div>
        <div class="recipe-meta">
          <h3 class="recipe-title">${escapeHtml(recipe.title)}</h3>
          <div class="recipe-tags">
      
page 1 / 2next ›