Function bodies 62 total
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 conninit_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 rseed_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 updatingdo_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 Noneparse_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 Nonesend_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."""
returnmain 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 NoneCitation: 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;
bescape_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 groupsloadRecipes 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 ErroWant 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";
});
closeFormButtonElapplyFilters 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}.`);
}
}
awarenderList 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 ›