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
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:
+169
-5
@@ -1397,6 +1397,114 @@ async function _loadConfigPage() {
|
|||||||
}
|
}
|
||||||
await _loadCategoriesConfigSection();
|
await _loadCategoriesConfigSection();
|
||||||
await _loadSubcategoryConfigSection();
|
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() {
|
async function _loadCategoriesConfigSection() {
|
||||||
@@ -14991,6 +15099,20 @@ async function loadRecipeArchive() {
|
|||||||
|
|
||||||
// ===== RECIPE LIBRARY (recettes manuelles, ex: cocktails) =====
|
// ===== 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 = [];
|
let _recipeLibraryCache = [];
|
||||||
|
|
||||||
async function loadRecipeLibrary() {
|
async function loadRecipeLibrary() {
|
||||||
@@ -15004,23 +15126,50 @@ async function loadRecipeLibrary() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
_recipeLibraryCache = result.recipes;
|
_recipeLibraryCache = result.recipes;
|
||||||
renderRecipeLibraryList(result.recipes);
|
renderRecipeLibraryTagFilterBar();
|
||||||
|
renderRecipeLibraryList(_recipeLibraryCache);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
container.innerHTML = `<p class="settings-hint">Erreur de chargement.</p>`;
|
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) {
|
function renderRecipeLibraryList(recipes) {
|
||||||
const container = document.getElementById('recipe-library-list');
|
const container = document.getElementById('recipe-library-list');
|
||||||
if (!container) return;
|
if (!container) return;
|
||||||
if (!recipes || recipes.length === 0) {
|
const filtered = _recipeLibraryActiveTagFilter
|
||||||
container.innerHTML = `<p class="settings-hint">Aucune recette pour l'instant.</p>`;
|
? 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;
|
return;
|
||||||
}
|
}
|
||||||
container.innerHTML = recipes.map(entry => {
|
container.innerHTML = filtered.map(entry => {
|
||||||
const r = entry.recipe;
|
const r = entry.recipe;
|
||||||
const favBadge = entry.is_favorite ? `<span class="recipe-fav-badge" title="Favori">★</span>` : '';
|
const favBadge = entry.is_favorite ? `<span class="recipe-fav-badge" title="Favori">★</span>` : '';
|
||||||
const ingCount = (r.ingredients || []).length;
|
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 `
|
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${entry.is_favorite ? ' recipe-archive-card-fav' : ''}" style="cursor:pointer" onclick="viewRecipeLibraryItem(${entry.id})">
|
||||||
<div class="recipe-archive-card-header">
|
<div class="recipe-archive-card-header">
|
||||||
@@ -15031,6 +15180,7 @@ function renderRecipeLibraryList(recipes) {
|
|||||||
<div class="recipe-archive-card-meta">
|
<div class="recipe-archive-card-meta">
|
||||||
<span>${ingCount} ingrédient${ingCount > 1 ? 's' : ''}</span>
|
<span>${ingCount} ingrédient${ingCount > 1 ? 's' : ''}</span>
|
||||||
</div>
|
</div>
|
||||||
|
${tagBadges ? `<div style="margin-top:6px;display:flex;flex-wrap:wrap;gap:4px">${tagBadges}</div>` : ''}
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
}).join('');
|
}).join('');
|
||||||
@@ -15075,6 +15225,15 @@ function openRecipeLibraryForm(id = null) {
|
|||||||
</div>
|
</div>
|
||||||
<button type="button" class="btn btn-small btn-secondary" onclick="addRecipeLibraryRow('rl-steps-list', 'step')">➕ Étape</button>
|
<button type="button" class="btn btn-small btn-secondary" onclick="addRecipeLibraryRow('rl-steps-list', 'step')">➕ Étape</button>
|
||||||
</div>
|
</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>
|
<button type="submit" class="btn btn-large btn-primary full-width">💾 Enregistrer</button>
|
||||||
</form>
|
</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 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);
|
showLoading(true);
|
||||||
try {
|
try {
|
||||||
@@ -20266,6 +20427,9 @@ async function _initApp() {
|
|||||||
|
|
||||||
// Load custom subcategories (merges into SUBCATEGORIES_BY_CATEGORY before any UI renders)
|
// Load custom subcategories (merges into SUBCATEGORIES_BY_CATEGORY before any UI renders)
|
||||||
await loadCustomSubcategories();
|
await loadCustomSubcategories();
|
||||||
|
|
||||||
|
// Load recipe tags (used by the "Mes recettes" form and filter bar)
|
||||||
|
await loadRecipeTags();
|
||||||
|
|
||||||
// Check for setup wizard resume (after language change)
|
// Check for setup wizard resume (after language change)
|
||||||
const resumeStep = localStorage.getItem('evershelf_setup_step');
|
const resumeStep = localStorage.getItem('evershelf_setup_step');
|
||||||
|
|||||||
Reference in New Issue
Block a user