Actualiser assets/js/app.js
CI / PHP Syntax Check (push) Has been cancelled
CI / JavaScript Lint (push) Has been cancelled
CI / Docker Build Test (push) Has been cancelled
CI / Validate Translation Files (push) Has been cancelled
CI / Auto-merge develop → main (push) Has been cancelled
CI / Create GitHub Release (push) Has been cancelled

This commit is contained in:
2026-06-18 13:05:58 +00:00
parent 9075740454
commit 8c471c1b7f
+169 -5
View File
@@ -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 = `<p class="settings-hint">Chargement…</p>`;
try {
const result = await api('recipe_tags_list', {}, 'GET');
if (!result.success) {
container.innerHTML = `<p class="settings-hint">Erreur de chargement.</p>`;
return;
}
RECIPE_TAGS = result.tags;
renderRecipeTagsConfigList(result.tags);
} catch (e) {
container.innerHTML = `<p class="settings-hint">Erreur de chargement.</p>`;
}
}
function renderRecipeTagsConfigList(tags) {
const container = document.getElementById('recipe-tags-list-container');
if (!container) return;
if (!tags || tags.length === 0) {
container.innerHTML = `<p class="settings-hint">Aucun tag.</p>`;
return;
}
container.innerHTML = tags.map(tag => `
<div class="loc-row" style="display:flex;align-items:center;gap:8px;padding:8px 0;border-bottom:1px solid var(--border,#e2e8f0)">
<input type="text" class="form-input" style="max-width:60px;text-align:center" value="${escapeHtml(tag.icon)}" id="rtag-icon-${tag.key}" maxlength="4">
<input type="text" class="form-input" style="flex:1" value="${escapeHtml(tag.label)}" id="rtag-label-${tag.key}">
<button class="btn btn-small btn-primary" onclick="updateRecipeTagConfig('${tag.key}')" title="Enregistrer">💾</button>
<button class="btn btn-small btn-secondary" onclick="removeRecipeTagConfig('${tag.key}')" title="Supprimer">🗑</button>
</div>
`).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 = `<p class="settings-hint">Erreur de chargement.</p>`;
}
}
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 `<button type="button" class="btn btn-small ${active ? 'btn-primary' : 'btn-secondary'}" onclick="setRecipeLibraryTagFilter('${tag.key}')">${tag.icon} ${escapeHtml(tag.label)}</button>`;
}).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 = `<p class="settings-hint">Aucune recette pour l'instant.</p>`;
const filtered = _recipeLibraryActiveTagFilter
? recipes.filter(entry => (entry.recipe.tags || []).includes(_recipeLibraryActiveTagFilter))
: recipes;
if (!filtered || filtered.length === 0) {
container.innerHTML = `<p class="settings-hint">Aucune recette${_recipeLibraryActiveTagFilter ? ' avec ce tag' : ' pour l\\'instant'}.</p>`;
return;
}
container.innerHTML = recipes.map(entry => {
container.innerHTML = filtered.map(entry => {
const r = entry.recipe;
const favBadge = entry.is_favorite ? `<span class="recipe-fav-badge" title="Favori">★</span>` : '';
const ingCount = (r.ingredients || []).length;
const tagBadges = (r.tags || []).map(k => {
const tag = RECIPE_TAGS.find(t => t.key === k);
return tag ? `<span class="inv-badge badge-subcategory">${tag.icon} ${escapeHtml(tag.label)}</span>` : '';
}).join('');
return `
<div class="recipe-archive-card${entry.is_favorite ? ' recipe-archive-card-fav' : ''}" style="cursor:pointer" onclick="viewRecipeLibraryItem(${entry.id})">
<div class="recipe-archive-card-header">
@@ -15031,6 +15180,7 @@ function renderRecipeLibraryList(recipes) {
<div class="recipe-archive-card-meta">
<span>${ingCount} ingrédient${ingCount > 1 ? 's' : ''}</span>
</div>
${tagBadges ? `<div style="margin-top:6px;display:flex;flex-wrap:wrap;gap:4px">${tagBadges}</div>` : ''}
</div>
`;
}).join('');
@@ -15075,6 +15225,15 @@ function openRecipeLibraryForm(id = null) {
</div>
<button type="button" class="btn btn-small btn-secondary" onclick="addRecipeLibraryRow('rl-steps-list', 'step')"> Étape</button>
</div>
<div class="form-group">
<label>Tags</label>
<div id="rl-tags-picker" style="display:flex;flex-wrap:wrap;gap:6px">
${RECIPE_TAGS.map(tag => {
const selected = (r.tags || []).includes(tag.key);
return `<button type="button" class="btn btn-small ${selected ? 'btn-primary' : 'btn-secondary'} rl-tag-chip" data-tag="${tag.key}" onclick="this.classList.toggle('btn-primary');this.classList.toggle('btn-secondary')">${tag.icon} ${escapeHtml(tag.label)}</button>`;
}).join('')}
</div>
</div>
<button type="submit" class="btn btn-large btn-primary full-width">💾 Enregistrer</button>
</form>
`;
@@ -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');