${recipes.length ? recipes.map(r => {
@@ -1449,6 +1453,20 @@ async function renderRecipes(container) {
container.querySelector("#add-recipe-btn").onclick = () => showRecipeEditorModal(null);
+ container.querySelector("#export-recipes-btn").onclick = async () => {
+ const allRecipes = await exportAllRecipes();
+ const json = JSON.stringify(allRecipes, null, 2);
+ downloadFile("baldav-recipes.json", json, "application/json");
+ };
+
+ container.querySelector("#import-recipes-btn").onclick = () => {
+ pickFile(".json", async (text) => {
+ const data = JSON.parse(text);
+ await importRecipes(data);
+ renderRecipes(container);
+ });
+ };
+
container.querySelectorAll(".recipe-card").forEach(card => {
card.onclick = () => {
const rid = parseInt(card.dataset.rid);
@@ -1519,6 +1537,7 @@ async function showRecipeDetailModal(recipeId) {
+
`, () => {});
@@ -1526,6 +1545,7 @@ async function showRecipeDetailModal(recipeId) {
const overlay = [...document.querySelectorAll(".modal-overlay")].pop();
const logBtn = overlay.querySelector("#rd-log");
const editBtn = overlay.querySelector("#rd-edit");
+ const exportBtn = overlay.querySelector("#rd-export");
const delBtn = overlay.querySelector("#rd-delete");
logBtn.onclick = async () => {
@@ -1536,8 +1556,22 @@ async function showRecipeDetailModal(recipeId) {
renderScreen(state.screen);
};
+ exportBtn.onclick = async () => {
+ const ings = await DB.getRecipeIngredients(recipeId);
+ const exported = {
+ name: recipe.name, cooking_method_id: recipe.cooking_method_id,
+ servings: recipe.servings, notes: recipe.notes,
+ ingredients: ings.map(i => ({
+ product_name: i.product_name, custom_name: i.custom_name,
+ amount_g: i.amount_g, cooking_method_id: i.cooking_method_id,
+ kcal_per_100g: i.kcal_per_100g, protein_per_100g: i.protein_per_100g,
+ fat_per_100g: i.fat_per_100g, carbs_per_100g: i.carbs_per_100g,
+ })),
+ };
+ downloadFile(`${recipe.name}.json`, JSON.stringify(exported, null, 2), "application/json");
+ };
+
editBtn.onclick = () => {
- // Close this modal via winStack, then open editor
if (winStack.length > 0) winStack.pop().close();
_skipPops = 1;
history.back();
@@ -3397,7 +3431,11 @@ async function renderSupplements(container) {
${supps.length ? supps.map(s => `
@@ -3415,6 +3453,19 @@ async function renderSupplements(container) {
container.querySelector("#add-new-supp").onclick = () => showEditSupplementModal(null);
+ container.querySelector("#export-supps-btn").onclick = async () => {
+ const supps = await DB.getSupplements();
+ downloadFile("baldav-supplements.json", JSON.stringify(supps, null, 2), "application/json");
+ };
+
+ container.querySelector("#import-supps-btn").onclick = () => {
+ pickFile(".json", async (text) => {
+ const data = JSON.parse(text);
+ await importSupplements(data);
+ renderSupplements(container);
+ });
+ };
+
container.querySelectorAll(".prod-card[data-sid]").forEach(card => {
card.querySelector(".prod-card-header").onclick = async () => {
const sid = parseInt(card.dataset.sid);
@@ -4225,6 +4276,89 @@ function addZoomDisplay(map) {
ctrl.addTo(map);
}
+// ---------------------------------------------------------------------------
+// Export/Import: recipes, supplements
+// ---------------------------------------------------------------------------
+
+function downloadFile(filename, content, mimeType) {
+ const blob = new Blob([content], { type: mimeType || "application/json" });
+ const a = document.createElement("a");
+ a.href = URL.createObjectURL(blob);
+ a.download = filename;
+ document.body.appendChild(a);
+ a.click();
+ a.remove();
+}
+
+async function exportAllRecipes() {
+ const recipes = await DB.getRecipes();
+ const result = [];
+ for (const r of recipes) {
+ const ings = await DB.getRecipeIngredients(r.id);
+ result.push({
+ name: r.name,
+ cooking_method_id: r.cooking_method_id,
+ cooking_method_name: r.cooking_method_name,
+ servings: r.servings,
+ notes: r.notes,
+ ingredients: ings.map(i => ({
+ product_name: i.product_name,
+ custom_name: i.custom_name,
+ amount_g: i.amount_g,
+ cooking_method_id: i.cooking_method_id,
+ cooking_method_name: i.cooking_method_name,
+ // Store product nutrients so recipient can create product if missing
+ kcal_per_100g: i.kcal_per_100g,
+ protein_per_100g: i.protein_per_100g,
+ fat_per_100g: i.fat_per_100g,
+ carbs_per_100g: i.carbs_per_100g,
+ })),
+ });
+ }
+ return result;
+}
+
+async function importRecipes(data) {
+ const arr = Array.isArray(data) ? data : [data];
+ for (const r of arr) {
+ if (!r.name) continue;
+ const rid = await DB.saveRecipe(r.name, r.cooking_method_id ?? null, r.servings ?? 1, r.notes ?? null);
+ if (!rid) continue;
+ for (const ing of (r.ingredients || [])) {
+ // Try to find product by name
+ let pid = 0;
+ if (ing.product_name) {
+ const found = await DB.searchProducts(ing.product_name, 1);
+ if (found.length) {
+ pid = found[0].id;
+ } else if (ing.kcal_per_100g != null) {
+ // Create product from exported data
+ pid = await DB.addProduct({
+ name: ing.product_name,
+ kcal_per_100g: ing.kcal_per_100g,
+ protein_per_100g: ing.protein_per_100g,
+ fat_per_100g: ing.fat_per_100g,
+ carbs_per_100g: ing.carbs_per_100g,
+ source: "recipe_import",
+ });
+ }
+ }
+ await DB.addRecipeIngredient(rid, pid, ing.custom_name ?? null, ing.amount_g ?? 100, ing.cooking_method_id ?? null);
+ }
+ }
+}
+
+async function importSupplements(data) {
+ const arr = Array.isArray(data) ? data : [data];
+ for (const s of arr) {
+ if (!s.name) continue;
+ // Remove id — will get new one
+ const clean = { ...s };
+ delete clean.id;
+ await DB.addSupplement(clean);
+ }
+}
+
function pickFile(accept, callback, asBinary) {
console.log("[pickFile] accept:", accept, "asBinary:", asBinary);
const input = document.createElement("input");