Update planner search and planner editor
This commit is contained in:
@@ -1,25 +1,12 @@
|
||||
import { RECIPES } from '../data/catalog.js?v=8';
|
||||
import { MEAL_SLOTS } from '../planner/mealSlots.js';
|
||||
import { getRecipeGridSectionHTML, renderRecipeGrid } from '../ui/recipeGrid.js';
|
||||
import {
|
||||
getRecipeSearchFieldHTML,
|
||||
syncRecipeSearchShellShadow,
|
||||
} from '../ui/recipeSearchField.js';
|
||||
|
||||
function escapeHtml(s) {
|
||||
return String(s)
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"');
|
||||
}
|
||||
|
||||
const slotLabelMap = Object.fromEntries(MEAL_SLOTS.map((s) => [s.id, s.label]));
|
||||
const DEFAULT_MIN_MINUTES = 5;
|
||||
const DEFAULT_MAX_MINUTES = 120;
|
||||
const SEARCH_SHELL_BASE_SHADOW =
|
||||
'0 5px 10px rgba(0,0,0,0.16), 0 14px 22px rgba(0,0,0,0.24), 0 22px 34px rgba(0,0,0,0.18), inset 0 1px 0 rgba(255,255,255,0.04)';
|
||||
|
||||
function slotLabelsFor(recipe) {
|
||||
return (recipe.allowedSlots || [])
|
||||
.map((id) => slotLabelMap[id])
|
||||
.filter(Boolean);
|
||||
}
|
||||
|
||||
let filterState = {
|
||||
query: '',
|
||||
@@ -57,107 +44,49 @@ function getFilteredRecipes() {
|
||||
return Object.values(RECIPES).filter(matchesFilters);
|
||||
}
|
||||
|
||||
function renderRecipeCard(recipe) {
|
||||
const labels = slotLabelsFor(recipe);
|
||||
return `
|
||||
<div data-recipe-id="${escapeHtml(recipe.id)}" onclick="openRecipeDetail('${escapeHtml(recipe.id)}')" class="recipe-card rounded-xl overflow-hidden flex flex-col bg-[#393937] cursor-pointer transition-shadow" style="background:#393937 !important; border:none !important; box-shadow:0 2px 8px rgba(0,0,0,0.28) !important;">
|
||||
<div class="h-32 bg-[#d4d4d4] relative overflow-hidden">
|
||||
${recipe.image
|
||||
? `<img src="${escapeHtml(recipe.image)}" alt="${escapeHtml(recipe.title)}" class="w-full h-full object-cover">`
|
||||
: `<span class="absolute inset-0 flex items-center justify-center text-white font-medium text-xs">${escapeHtml(recipe.thumbLabel)}</span>`}
|
||||
</div>
|
||||
<div class="p-3 flex flex-col flex-1">
|
||||
<h3 class="text-sm font-medium underline decoration-1 underline-offset-2 text-[#f1ede4] mb-3 line-clamp-2">${escapeHtml(recipe.title)}</h3>
|
||||
<div class="mt-auto">
|
||||
<div class="flex items-center justify-between text-[11px] text-[#c2bcb2] font-medium mb-2">
|
||||
<div class="flex items-center gap-1"><i class="fas fa-clock text-[#8f8b84]"></i><span>${recipe.minutes} min</span></div>
|
||||
<div class="flex items-center gap-1"><i class="fas fa-fire text-[#8f8b84]"></i><span>${recipe.nutritionPerServing.kcal} kcal</span></div>
|
||||
</div>
|
||||
<div class="flex flex-wrap gap-1">
|
||||
${labels.map((l) => `<span class="px-2 py-0.5 bg-[#2f2f2d] text-[#d7d2c8] text-[10px] rounded-md font-medium">${escapeHtml(l)}</span>`).join('')}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
function getEmptyStateHTML() {
|
||||
return `
|
||||
<div id="recipe-empty-state" class="hidden flex flex-col items-center justify-center py-16 text-center">
|
||||
<div class="w-16 h-16 rounded-full bg-gray-100 flex items-center justify-center mb-4">
|
||||
<i class="fas fa-search text-2xl text-gray-300"></i>
|
||||
</div>
|
||||
<p class="text-sm font-semibold text-gray-700">Brak wyników</p>
|
||||
<p class="text-xs text-gray-500 mt-1 max-w-[220px] leading-relaxed">Zmień kryteria wyszukiwania lub filtry</p>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
function syncRecipeScrollShadow() {
|
||||
const scroll = document.getElementById('recipe-scroll');
|
||||
const searchShell = document.getElementById('recipe-search-shell');
|
||||
if (!searchShell) return;
|
||||
|
||||
if (!scroll) {
|
||||
searchShell.style.boxShadow = SEARCH_SHELL_BASE_SHADOW;
|
||||
return;
|
||||
}
|
||||
|
||||
searchShell.style.boxShadow = SEARCH_SHELL_BASE_SHADOW;
|
||||
syncRecipeSearchShellShadow(searchShell);
|
||||
}
|
||||
|
||||
function renderAllRecipeCards() {
|
||||
const grid = document.getElementById('recipe-grid');
|
||||
if (!grid) return;
|
||||
|
||||
grid.innerHTML = Object.values(RECIPES).map(renderRecipeCard).join('');
|
||||
}
|
||||
|
||||
function syncVisibleRecipeCards() {
|
||||
function renderGrid() {
|
||||
const grid = document.getElementById('recipe-grid');
|
||||
const emptyState = document.getElementById('recipe-empty-state');
|
||||
if (!grid || !emptyState) return;
|
||||
|
||||
let visibleCount = 0;
|
||||
grid.querySelectorAll('[data-recipe-id]').forEach((card) => {
|
||||
const recipeId = card.getAttribute('data-recipe-id');
|
||||
const recipe = recipeId ? RECIPES[recipeId] : null;
|
||||
const isVisible = Boolean(recipe && matchesFilters(recipe));
|
||||
card.classList.toggle('hidden', !isVisible);
|
||||
if (isVisible) visibleCount += 1;
|
||||
});
|
||||
|
||||
grid.classList.toggle('hidden', visibleCount === 0);
|
||||
emptyState.classList.toggle('hidden', visibleCount !== 0);
|
||||
requestAnimationFrame(syncRecipeScrollShadow);
|
||||
}
|
||||
|
||||
function renderGrid({ rebuild = false } = {}) {
|
||||
const grid = document.getElementById('recipe-grid');
|
||||
if (!grid) return;
|
||||
|
||||
if (rebuild || !grid.querySelector('[data-recipe-id]')) {
|
||||
renderAllRecipeCards();
|
||||
}
|
||||
|
||||
syncVisibleRecipeCards();
|
||||
renderRecipeGrid({
|
||||
gridEl: grid,
|
||||
emptyStateEl: emptyState,
|
||||
recipes: getFilteredRecipes(),
|
||||
});
|
||||
requestAnimationFrame(syncRecipeScrollShadow);
|
||||
}
|
||||
|
||||
export function getRecipeListHTML() {
|
||||
return `
|
||||
<div id="main-view" class="flex flex-col h-full absolute inset-0 bg-[#2d2e2b] z-10" style="background:#2d2e2b !important;">
|
||||
<div id="recipe-top-bar" class="pointer-events-none absolute inset-x-0 top-0 z-[12] px-4 pt-4" style="background:transparent !important; border:none !important;">
|
||||
<div id="recipe-search-shell" class="pointer-events-auto relative z-[1] mx-auto flex items-center w-full overflow-hidden" style="width:min(calc(100% - 0.5rem), 22.4rem); background:#393937 !important; border:1px solid #41423f !important; border-radius:999px !important; box-shadow:${SEARCH_SHELL_BASE_SHADOW} !important; transition:box-shadow 180ms ease;">
|
||||
<input type="text" id="recipe-search-input" placeholder="Szukaj przepisów..." class="w-full bg-transparent outline-none text-[15px] text-center py-[12px] pl-8 pr-14" style="background:transparent !important; border:none !important; box-shadow:none !important; backdrop-filter:none !important;">
|
||||
<button id="recipe-filter-btn" onclick="openFilters()" class="absolute right-2 top-1/2 -translate-y-1/2 w-9 h-9 text-[#c9c3b8] hover:text-[#f0e8dc] flex items-center justify-center transition-colors" style="background:transparent !important; border:none !important; box-shadow:none !important;" aria-label="Otwórz filtry">
|
||||
<i class="fas fa-sliders-h"></i>
|
||||
</button>
|
||||
<div class="pointer-events-auto">
|
||||
${getRecipeSearchFieldHTML({
|
||||
shellId: 'recipe-search-shell',
|
||||
inputId: 'recipe-search-input',
|
||||
placeholder: 'Szukaj przepisów...',
|
||||
filterButtonId: 'recipe-filter-btn',
|
||||
filterButtonAction: 'openFilters()',
|
||||
filterButtonLabel: 'Otwórz filtry',
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="recipe-scroll" class="relative flex-1 overflow-y-auto px-4 pt-20 pb-24 bg-[#2d2e2b]" style="background:#2d2e2b !important;">
|
||||
<div id="recipe-grid" class="grid grid-cols-2 gap-3 bg-[#2d2e2b]" style="background:#2d2e2b !important;"></div>
|
||||
${getEmptyStateHTML()}
|
||||
</div>
|
||||
${getRecipeGridSectionHTML({
|
||||
scrollId: 'recipe-scroll',
|
||||
gridId: 'recipe-grid',
|
||||
emptyStateId: 'recipe-empty-state',
|
||||
scrollClassName: 'relative flex-1 overflow-y-auto px-4 pt-20 pb-24 bg-[#2d2e2b]',
|
||||
gridClassName: 'grid grid-cols-2 gap-3 bg-[#2d2e2b]',
|
||||
emptyTitle: 'Brak wyników',
|
||||
emptyMessage: 'Zmień kryteria wyszukiwania lub filtry',
|
||||
})}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
@@ -176,17 +105,24 @@ export function getFilteredCount() {
|
||||
}
|
||||
|
||||
export function refreshRecipeList() {
|
||||
renderGrid({ rebuild: true });
|
||||
renderGrid();
|
||||
}
|
||||
|
||||
export function setupRecipeList() {
|
||||
renderGrid({ rebuild: true });
|
||||
renderGrid();
|
||||
|
||||
document.getElementById('recipe-search-input')?.addEventListener('input', (e) => {
|
||||
filterState.query = e.target.value.trim();
|
||||
renderGrid();
|
||||
});
|
||||
|
||||
document.getElementById('recipe-grid')?.addEventListener('click', (e) => {
|
||||
const card = e.target.closest('.recipe-browser-card');
|
||||
if (!card) return;
|
||||
const recipeId = card.getAttribute('data-recipe-id');
|
||||
if (recipeId) window.openRecipeDetail?.(recipeId);
|
||||
});
|
||||
|
||||
document.getElementById('recipe-scroll')?.addEventListener('scroll', syncRecipeScrollShadow);
|
||||
syncRecipeScrollShadow();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user