diff --git a/assets/js/app.js b/assets/js/app.js
index c6e479b..cb4f6cb 100644
--- a/assets/js/app.js
+++ b/assets/js/app.js
@@ -1397,6 +1397,114 @@ async function _loadConfigPage() {
}
await _loadCategoriesConfigSection();
await _loadSubcategoryConfigSection();
+ await _loadRecipeTagsConfigSection();
+}
+
+async function _loadRecipeTagsConfigSection() {
+ const container = document.getElementById('recipe-tags-list-container');
+ if (!container) return;
+ container.innerHTML = `
Chargement…
`;
+ try {
+ const result = await api('recipe_tags_list', {}, 'GET');
+ if (!result.success) {
+ container.innerHTML = `Erreur de chargement.
`;
+ return;
+ }
+ RECIPE_TAGS = result.tags;
+ renderRecipeTagsConfigList(result.tags);
+ } catch (e) {
+ container.innerHTML = `Erreur de chargement.
`;
+ }
+}
+
+function renderRecipeTagsConfigList(tags) {
+ const container = document.getElementById('recipe-tags-list-container');
+ if (!container) return;
+ if (!tags || tags.length === 0) {
+ container.innerHTML = `Aucun tag.
`;
+ return;
+ }
+ container.innerHTML = tags.map(tag => `
+
+
+
+
+
+
+ `).join('');
+}
+
+async function addRecipeTagConfig() {
+ const iconInput = document.getElementById('new-recipe-tag-icon');
+ const labelInput = document.getElementById('new-recipe-tag-label');
+ const icon = iconInput.value.trim() || '🏷️';
+ const label = labelInput.value.trim();
+
+ if (!label) {
+ showToast('Indique un nom pour le nouveau tag', 'warning');
+ return;
+ }
+
+ showLoading(true);
+ try {
+ const result = await api('recipe_tags_add', {}, 'POST', { label, icon });
+ showLoading(false);
+ if (result.success) {
+ showToast(`Tag "${label}" ajouté`, 'success');
+ iconInput.value = '';
+ labelInput.value = '';
+ _loadRecipeTagsConfigSection();
+ } else {
+ showToast(result.error || 'Erreur lors de l\\'ajout', 'error');
+ }
+ } catch (e) {
+ showLoading(false);
+ showToast('Erreur lors de l\\'ajout', 'error');
+ }
+}
+
+async function updateRecipeTagConfig(key) {
+ const icon = document.getElementById(`rtag-icon-${key}`).value.trim() || '🏷️';
+ const label = document.getElementById(`rtag-label-${key}`).value.trim();
+
+ if (!label) {
+ showToast('Le nom ne peut pas être vide', 'warning');
+ return;
+ }
+
+ showLoading(true);
+ try {
+ const result = await api('recipe_tags_update', {}, 'POST', { key, label, icon });
+ showLoading(false);
+ if (result.success) {
+ showToast('Tag mis à jour', 'success');
+ _loadRecipeTagsConfigSection();
+ } else {
+ showToast(result.error || 'Erreur', 'error');
+ }
+ } catch (e) {
+ showLoading(false);
+ showToast('Erreur', 'error');
+ }
+}
+
+async function removeRecipeTagConfig(key) {
+ if (!confirm(`Supprimer ce tag ?`)) return;
+
+ showLoading(true);
+ try {
+ const result = await api('recipe_tags_remove', {}, 'POST', { key });
+ showLoading(false);
+ if (result.success) {
+ showToast('Tag supprimé', 'success');
+ _loadRecipeTagsConfigSection();
+ } else {
+ showToast(result.error || 'Impossible de supprimer', 'error');
+ }
+ } catch (e) {
+ showLoading(false);
+ showToast('Erreur lors de la suppression', 'error');
+ }
}
async function _loadCategoriesConfigSection() {
@@ -14991,6 +15099,20 @@ async function loadRecipeArchive() {
// ===== RECIPE LIBRARY (recettes manuelles, ex: cocktails) =====
+let RECIPE_TAGS = [];
+let _recipeLibraryActiveTagFilter = null;
+
+async function loadRecipeTags() {
+ try {
+ const result = await api('recipe_tags_list', {}, 'GET');
+ if (result.success && Array.isArray(result.tags)) {
+ RECIPE_TAGS = result.tags;
+ }
+ } catch (e) {
+ console.warn('[EverShelf] Could not load recipe tags:', e);
+ }
+}
+
let _recipeLibraryCache = [];
async function loadRecipeLibrary() {
@@ -15004,23 +15126,50 @@ async function loadRecipeLibrary() {
return;
}
_recipeLibraryCache = result.recipes;
- renderRecipeLibraryList(result.recipes);
+ renderRecipeLibraryTagFilterBar();
+ renderRecipeLibraryList(_recipeLibraryCache);
} catch (e) {
container.innerHTML = `Erreur de chargement.
`;
}
}
+function renderRecipeLibraryTagFilterBar() {
+ const container = document.getElementById('recipe-library-tag-filter');
+ if (!container) return;
+ const usedKeys = new Set();
+ _recipeLibraryCache.forEach(entry => (entry.recipe.tags || []).forEach(k => usedKeys.add(k)));
+ const usedTags = RECIPE_TAGS.filter(t => usedKeys.has(t.key));
+ if (usedTags.length === 0) { container.innerHTML = ''; return; }
+ container.innerHTML = usedTags.map(tag => {
+ const active = _recipeLibraryActiveTagFilter === tag.key;
+ return ``;
+ }).join('');
+}
+
+function setRecipeLibraryTagFilter(key) {
+ _recipeLibraryActiveTagFilter = (_recipeLibraryActiveTagFilter === key) ? null : key;
+ renderRecipeLibraryTagFilterBar();
+ renderRecipeLibraryList(_recipeLibraryCache);
+}
+
function renderRecipeLibraryList(recipes) {
const container = document.getElementById('recipe-library-list');
if (!container) return;
- if (!recipes || recipes.length === 0) {
- container.innerHTML = `Aucune recette pour l'instant.
`;
+ const filtered = _recipeLibraryActiveTagFilter
+ ? recipes.filter(entry => (entry.recipe.tags || []).includes(_recipeLibraryActiveTagFilter))
+ : recipes;
+ if (!filtered || filtered.length === 0) {
+ container.innerHTML = `Aucune recette${_recipeLibraryActiveTagFilter ? ' avec ce tag' : ' pour l\\'instant'}.
`;
return;
}
- container.innerHTML = recipes.map(entry => {
+ container.innerHTML = filtered.map(entry => {
const r = entry.recipe;
const favBadge = entry.is_favorite ? `★` : '';
const ingCount = (r.ingredients || []).length;
+ const tagBadges = (r.tags || []).map(k => {
+ const tag = RECIPE_TAGS.find(t => t.key === k);
+ return tag ? `${tag.icon} ${escapeHtml(tag.label)}` : '';
+ }).join('');
return `
`;
}).join('');
@@ -15075,6 +15225,15 @@ function openRecipeLibraryForm(id = null) {
+
`;
@@ -15114,7 +15273,9 @@ async function submitRecipeLibraryForm(e, id) {
const steps = Array.from(document.querySelectorAll('#rl-steps-list .rl-step-text')).map(input => input.value.trim()).filter(Boolean);
- const recipe = { title, ingredients, steps, persons: 1 };
+ const tags = Array.from(document.querySelectorAll('#rl-tags-picker .rl-tag-chip.btn-primary')).map(b => b.dataset.tag);
+
+ const recipe = { title, ingredients, steps, tags, persons: 1 };
showLoading(true);
try {
@@ -20266,6 +20427,9 @@ async function _initApp() {
// Load custom subcategories (merges into SUBCATEGORIES_BY_CATEGORY before any UI renders)
await loadCustomSubcategories();
+
+ // Load recipe tags (used by the "Mes recettes" form and filter bar)
+ await loadRecipeTags();
// Check for setup wizard resume (after language change)
const resumeStep = localStorage.getItem('evershelf_setup_step');