Compare commits
2 Commits
4706430316
...
6bf50f67ad
| Author | SHA1 | Date | |
|---|---|---|---|
| 6bf50f67ad | |||
| 165f39d0b7 |
112
index.html
112
index.html
@@ -11,7 +11,7 @@
|
|||||||
<meta http-equiv="Pragma" content="no-cache">
|
<meta http-equiv="Pragma" content="no-cache">
|
||||||
<meta http-equiv="Expires" content="0">
|
<meta http-equiv="Expires" content="0">
|
||||||
<title>Recipe App - Modular</title>
|
<title>Recipe App - Modular</title>
|
||||||
<link rel="manifest" href="./manifest.webmanifest?v=20260407-66">
|
<link rel="manifest" href="./manifest.webmanifest?v=20260408-97">
|
||||||
<link rel="icon" type="image/png" sizes="192x192" href="./icons/icon-192.png">
|
<link rel="icon" type="image/png" sizes="192x192" href="./icons/icon-192.png">
|
||||||
<link rel="apple-touch-icon" href="./icons/apple-touch-icon.png">
|
<link rel="apple-touch-icon" href="./icons/apple-touch-icon.png">
|
||||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||||
@@ -233,9 +233,16 @@
|
|||||||
}
|
}
|
||||||
#main-view,
|
#main-view,
|
||||||
#main-view > div:last-child,
|
#main-view > div:last-child,
|
||||||
#recipe-grid {
|
#recipe-grid,
|
||||||
|
#planner-picker-grid {
|
||||||
background: #2d2e2b !important;
|
background: #2d2e2b !important;
|
||||||
}
|
}
|
||||||
|
#recipe-grid,
|
||||||
|
#planner-picker-grid {
|
||||||
|
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||||
|
gap: 0.5rem !important;
|
||||||
|
align-items: stretch;
|
||||||
|
}
|
||||||
#planner-view,
|
#planner-view,
|
||||||
#planner-view > div:first-child,
|
#planner-view > div:first-child,
|
||||||
#planner-scroll,
|
#planner-scroll,
|
||||||
@@ -255,24 +262,82 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* Cards and sheets */
|
/* Cards and sheets */
|
||||||
#recipe-grid > div {
|
#recipe-grid > *,
|
||||||
|
#planner-picker-grid > * {
|
||||||
background: #393937 !important;
|
background: #393937 !important;
|
||||||
border: none !important;
|
border: none !important;
|
||||||
border-radius: 1.75rem !important;
|
border-radius: 1.75rem !important;
|
||||||
box-shadow: none !important;
|
box-shadow: none !important;
|
||||||
transition: transform 180ms ease, box-shadow 180ms ease !important;
|
transition: transform 180ms ease, box-shadow 180ms ease !important;
|
||||||
}
|
}
|
||||||
#recipe-grid > div:hover {
|
#recipe-grid > .recipe-list-card,
|
||||||
|
#planner-picker-grid > .recipe-list-card {
|
||||||
|
border-radius: 1.25rem !important;
|
||||||
|
height: 11.9rem;
|
||||||
|
}
|
||||||
|
#recipe-grid > .recipe-list-card .recipe-browser-card-media,
|
||||||
|
#planner-picker-grid > .recipe-list-card .recipe-browser-card-media {
|
||||||
|
height: 5.25rem;
|
||||||
|
}
|
||||||
|
#recipe-grid > .recipe-list-card .recipe-browser-card-body,
|
||||||
|
#planner-picker-grid > .recipe-list-card .recipe-browser-card-body {
|
||||||
|
padding: 0.58rem;
|
||||||
|
}
|
||||||
|
#recipe-grid > .recipe-list-card .recipe-browser-card-title,
|
||||||
|
#planner-picker-grid > .recipe-list-card .recipe-browser-card-title {
|
||||||
|
display: -webkit-box;
|
||||||
|
min-height: 2.76rem;
|
||||||
|
margin-bottom: 0.55rem;
|
||||||
|
font-size: 0.66rem;
|
||||||
|
font-weight: 400 !important;
|
||||||
|
line-height: 0.92rem;
|
||||||
|
overflow: hidden;
|
||||||
|
text-decoration: none !important;
|
||||||
|
-webkit-line-clamp: 3;
|
||||||
|
-webkit-box-orient: vertical;
|
||||||
|
}
|
||||||
|
#recipe-grid > .recipe-list-card .recipe-browser-card-meta,
|
||||||
|
#planner-picker-grid > .recipe-list-card .recipe-browser-card-meta {
|
||||||
|
margin-bottom: 0.45rem;
|
||||||
|
gap: 0.2rem;
|
||||||
|
font-size: 0.58rem;
|
||||||
|
line-height: 0.78rem;
|
||||||
|
flex-wrap: nowrap;
|
||||||
|
}
|
||||||
|
#recipe-grid > .recipe-list-card .recipe-browser-card-meta > div,
|
||||||
|
#planner-picker-grid > .recipe-list-card .recipe-browser-card-meta > div {
|
||||||
|
min-width: 0;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
#recipe-grid > .recipe-list-card .recipe-browser-card-meta i,
|
||||||
|
#planner-picker-grid > .recipe-list-card .recipe-browser-card-meta i {
|
||||||
|
font-size: 0.52rem;
|
||||||
|
}
|
||||||
|
#recipe-grid > .recipe-list-card .recipe-browser-card-labels,
|
||||||
|
#planner-picker-grid > .recipe-list-card .recipe-browser-card-labels {
|
||||||
|
gap: 0.25rem;
|
||||||
|
}
|
||||||
|
#recipe-grid > .recipe-list-card .recipe-browser-card-label,
|
||||||
|
#planner-picker-grid > .recipe-list-card .recipe-browser-card-label {
|
||||||
|
padding: 0.14rem 0.36rem;
|
||||||
|
font-size: 0.54rem;
|
||||||
|
line-height: 0.72rem;
|
||||||
|
}
|
||||||
|
#recipe-grid > *:hover,
|
||||||
|
#planner-picker-grid > *:hover {
|
||||||
transform: translateY(-2px);
|
transform: translateY(-2px);
|
||||||
box-shadow: none !important;
|
box-shadow: none !important;
|
||||||
}
|
}
|
||||||
#recipe-grid > div img {
|
#recipe-grid > * img,
|
||||||
|
#planner-picker-grid > * img {
|
||||||
transition: transform 240ms ease;
|
transition: transform 240ms ease;
|
||||||
}
|
}
|
||||||
#recipe-grid > div:hover img {
|
#recipe-grid > *:hover img,
|
||||||
|
#planner-picker-grid > *:hover img {
|
||||||
transform: scale(1.04);
|
transform: scale(1.04);
|
||||||
}
|
}
|
||||||
#recipe-grid > div > div:first-child::after,
|
#recipe-grid > * > div:first-child::after,
|
||||||
|
#planner-picker-grid > * > div:first-child::after,
|
||||||
#rd-hero::after {
|
#rd-hero::after {
|
||||||
content: '';
|
content: '';
|
||||||
position: absolute;
|
position: absolute;
|
||||||
@@ -280,7 +345,8 @@
|
|||||||
background: linear-gradient(180deg, rgba(0, 0, 0, 0.04), rgba(var(--app-bg-rgb), 0.5) 92%);
|
background: linear-gradient(180deg, rgba(0, 0, 0, 0.04), rgba(var(--app-bg-rgb), 0.5) 92%);
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
#recipe-search-shell {
|
#recipe-search-shell,
|
||||||
|
#planner-picker-search-shell {
|
||||||
min-height: 3rem;
|
min-height: 3rem;
|
||||||
width: min(calc(100% - 0.5rem), 22.4rem);
|
width: min(calc(100% - 0.5rem), 22.4rem);
|
||||||
margin-inline: auto;
|
margin-inline: auto;
|
||||||
@@ -294,11 +360,14 @@
|
|||||||
0 5px 10px rgba(0, 0, 0, 0.16),
|
0 5px 10px rgba(0, 0, 0, 0.16),
|
||||||
0 14px 22px rgba(0, 0, 0, 0.24),
|
0 14px 22px rgba(0, 0, 0, 0.24),
|
||||||
0 22px 34px rgba(0, 0, 0, 0.18),
|
0 22px 34px rgba(0, 0, 0, 0.18),
|
||||||
inset 0 1px 0 rgba(255, 255, 255, 0.04) !important;
|
inset 0 1px 0 rgba(255, 255, 255, 0.04),
|
||||||
|
inset 0 2px 6px rgba(0, 0, 0, 0.16),
|
||||||
|
inset 0 -1px 2px rgba(255, 255, 255, 0.02) !important;
|
||||||
backdrop-filter: blur(24px);
|
backdrop-filter: blur(24px);
|
||||||
-webkit-backdrop-filter: blur(24px);
|
-webkit-backdrop-filter: blur(24px);
|
||||||
}
|
}
|
||||||
#recipe-search-shell::after {
|
#recipe-search-shell::after,
|
||||||
|
#planner-picker-search-shell::after {
|
||||||
content: '';
|
content: '';
|
||||||
position: absolute;
|
position: absolute;
|
||||||
left: 11%;
|
left: 11%;
|
||||||
@@ -312,16 +381,20 @@
|
|||||||
z-index: -1;
|
z-index: -1;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
#recipe-search-shell:focus-within {
|
#recipe-search-shell:focus-within,
|
||||||
|
#planner-picker-search-shell:focus-within {
|
||||||
background: #393937 !important;
|
background: #393937 !important;
|
||||||
border: 1px solid #4a4b47 !important;
|
border: 1px solid #4a4b47 !important;
|
||||||
box-shadow:
|
box-shadow:
|
||||||
0 6px 12px rgba(0, 0, 0, 0.18),
|
0 6px 12px rgba(0, 0, 0, 0.18),
|
||||||
0 16px 24px rgba(0, 0, 0, 0.24),
|
0 16px 24px rgba(0, 0, 0, 0.24),
|
||||||
0 24px 36px rgba(0, 0, 0, 0.18),
|
0 24px 36px rgba(0, 0, 0, 0.18),
|
||||||
inset 0 1px 0 rgba(255, 255, 255, 0.05) !important;
|
inset 0 1px 0 rgba(255, 255, 255, 0.05),
|
||||||
|
inset 0 2px 7px rgba(0, 0, 0, 0.18),
|
||||||
|
inset 0 -1px 2px rgba(255, 255, 255, 0.03) !important;
|
||||||
}
|
}
|
||||||
#recipe-search-input {
|
#recipe-search-input,
|
||||||
|
#planner-picker-search {
|
||||||
appearance: none;
|
appearance: none;
|
||||||
-webkit-appearance: none;
|
-webkit-appearance: none;
|
||||||
background: transparent !important;
|
background: transparent !important;
|
||||||
@@ -334,17 +407,20 @@
|
|||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
letter-spacing: -0.02em;
|
letter-spacing: -0.02em;
|
||||||
}
|
}
|
||||||
#recipe-search-input::placeholder {
|
#recipe-search-input::placeholder,
|
||||||
|
#planner-picker-search::placeholder {
|
||||||
color: #beb8ae !important;
|
color: #beb8ae !important;
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
}
|
}
|
||||||
#recipe-filter-btn {
|
#recipe-filter-btn,
|
||||||
|
#planner-picker-filter-btn {
|
||||||
border-radius: 999px;
|
border-radius: 999px;
|
||||||
background: transparent !important;
|
background: transparent !important;
|
||||||
border: none !important;
|
border: none !important;
|
||||||
box-shadow: none !important;
|
box-shadow: none !important;
|
||||||
}
|
}
|
||||||
#recipe-filter-btn:hover {
|
#recipe-filter-btn:hover,
|
||||||
|
#planner-picker-filter-btn:hover {
|
||||||
background: rgba(255, 255, 255, 0.03) !important;
|
background: rgba(255, 255, 255, 0.03) !important;
|
||||||
}
|
}
|
||||||
#planner-picker-sheet,
|
#planner-picker-sheet,
|
||||||
@@ -524,7 +600,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
const APP_ASSET_VERSION = '20260407-66';
|
const APP_ASSET_VERSION = '20260408-97';
|
||||||
const APP_VERSION_STORAGE_KEY = 'recipe-app-asset-version';
|
const APP_VERSION_STORAGE_KEY = 'recipe-app-asset-version';
|
||||||
const APP_VERSION_QUERY_KEY = 'appv';
|
const APP_VERSION_QUERY_KEY = 'appv';
|
||||||
|
|
||||||
@@ -558,7 +634,7 @@
|
|||||||
})();
|
})();
|
||||||
</script>
|
</script>
|
||||||
<script type="module">
|
<script type="module">
|
||||||
const appVersion = window.__APP_ASSET_VERSION__ || '20260407-66';
|
const appVersion = window.__APP_ASSET_VERSION__ || '20260408-97';
|
||||||
const recoveryKey = `recipe-app-recovery-${appVersion}`;
|
const recoveryKey = `recipe-app-recovery-${appVersion}`;
|
||||||
|
|
||||||
function renderBootstrapError(message) {
|
function renderBootstrapError(message) {
|
||||||
|
|||||||
@@ -271,15 +271,16 @@ export function getMealPlanEditorHTML() {
|
|||||||
<div id="mpe-servings-row" class="mt-3"></div>
|
<div id="mpe-servings-row" class="mt-3"></div>
|
||||||
<div id="mpe-top-shadow" class="pointer-events-none absolute inset-x-0 -bottom-3 h-3 opacity-0 transition-opacity duration-200" style="background:linear-gradient(to bottom, rgba(0,0,0,0.12), rgba(0,0,0,0.03), rgba(0,0,0,0));"></div>
|
<div id="mpe-top-shadow" class="pointer-events-none absolute inset-x-0 -bottom-3 h-3 opacity-0 transition-opacity duration-200" style="background:linear-gradient(to bottom, rgba(0,0,0,0.12), rgba(0,0,0,0.03), rgba(0,0,0,0));"></div>
|
||||||
</div>
|
</div>
|
||||||
<div id="mpe-ing-scroll" class="flex-1 min-h-0 overflow-y-auto no-scrollbar px-5 pb-16 bg-[#2d2e2b]" style="background:#2d2e2b !important; background-image:none !important;">
|
<div id="mpe-ing-scroll" class="flex-1 min-h-0 overflow-y-auto no-scrollbar px-5 pb-24 bg-[#2d2e2b]" style="background:#2d2e2b !important; background-image:none !important;">
|
||||||
<div id="mpe-ing-section" class="mb-4">
|
<div id="mpe-ing-section" class="mb-4">
|
||||||
<p class="text-[10px] font-bold text-gray-400 uppercase tracking-wider mb-2">Składniki</p>
|
<p class="text-[10px] font-bold text-gray-400 uppercase tracking-wider mb-2">Składniki</p>
|
||||||
<div id="mpe-ing-list" class="space-y-1.5"></div>
|
<div id="mpe-ing-list" class="space-y-1.5"></div>
|
||||||
<div id="mpe-add-area" class="mt-2"></div>
|
<div id="mpe-add-area" class="mt-2"></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div id="mpe-footer-wrap" class="absolute bottom-0 left-0 right-0 z-[2] px-5 pb-3 flex justify-center" style="pointer-events:none; padding-bottom:calc(1.1rem + env(safe-area-inset-bottom));">
|
<div id="mpe-footer-wrap" class="absolute bottom-0 left-0 right-0 z-[2] px-5 pb-4 flex justify-center" style="pointer-events:none; padding-bottom:calc(1.7rem + env(safe-area-inset-bottom));">
|
||||||
<button id="mpe-confirm-btn" type="button" class="border text-white px-6 py-3 rounded-full font-semibold text-[13px] transition-colors inline-flex items-center justify-center gap-2" style="pointer-events:auto; background:#2d2e2b !important; background-image:none !important; border-color:#444442 !important; box-shadow:0 4px 16px rgba(0,0,0,0.4), 0 1px 4px rgba(0,0,0,0.25);">
|
<button id="mpe-confirm-btn" type="button" class="border h-10 px-9 rounded-full font-semibold text-[13px] transition-colors inline-flex items-center justify-center gap-2" style="pointer-events:auto; background:#dcd6cb !important; color:#2d2e2b !important; background-image:none !important; border-color:#dcd6cb !important; box-shadow:0 4px 16px rgba(0,0,0,0.28), 0 1px 4px rgba(0,0,0,0.2);">
|
||||||
|
<i id="mpe-confirm-icon" class="fas fa-calendar-plus text-[11px]" aria-hidden="true"></i>
|
||||||
<span id="mpe-confirm-label">Dodaj do planu</span>
|
<span id="mpe-confirm-label">Dodaj do planu</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -696,6 +697,12 @@ export function setupMealPlanEditor() {
|
|||||||
document.getElementById('mpe-title').textContent = S.mode === 'edit' ? 'Edytuj posiłek' : 'Zaplanuj posiłek';
|
document.getElementById('mpe-title').textContent = S.mode === 'edit' ? 'Edytuj posiłek' : 'Zaplanuj posiłek';
|
||||||
document.getElementById('mpe-subtitle').textContent = recipe.title;
|
document.getElementById('mpe-subtitle').textContent = recipe.title;
|
||||||
document.getElementById('mpe-confirm-label').textContent = S.mode === 'edit' ? 'Zapisz zmiany' : 'Dodaj do planu';
|
document.getElementById('mpe-confirm-label').textContent = S.mode === 'edit' ? 'Zapisz zmiany' : 'Dodaj do planu';
|
||||||
|
const confirmIcon = document.getElementById('mpe-confirm-icon');
|
||||||
|
if (confirmIcon) {
|
||||||
|
confirmIcon.className = S.mode === 'edit'
|
||||||
|
? 'fas fa-check text-[11px]'
|
||||||
|
: 'fas fa-calendar-plus text-[11px]';
|
||||||
|
}
|
||||||
|
|
||||||
renderAll();
|
renderAll();
|
||||||
const body = document.getElementById('mpe-ing-scroll');
|
const body = document.getElementById('mpe-ing-scroll');
|
||||||
|
|||||||
108
js/ui/recipeGrid.js
Normal file
108
js/ui/recipeGrid.js
Normal file
@@ -0,0 +1,108 @@
|
|||||||
|
import { MEAL_SLOTS } from '../planner/mealSlots.js';
|
||||||
|
|
||||||
|
function escapeHtml(s) {
|
||||||
|
return String(s)
|
||||||
|
.replace(/&/g, '&')
|
||||||
|
.replace(/</g, '<')
|
||||||
|
.replace(/>/g, '>')
|
||||||
|
.replace(/"/g, '"');
|
||||||
|
}
|
||||||
|
|
||||||
|
const slotLabelMap = Object.fromEntries(MEAL_SLOTS.map((slot) => [slot.id, slot.label]));
|
||||||
|
|
||||||
|
function slotLabelsFor(recipe) {
|
||||||
|
return (recipe.allowedSlots || [])
|
||||||
|
.map((id) => slotLabelMap[id])
|
||||||
|
.filter(Boolean);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getEmptyStateHTML({ emptyStateId, title, message }) {
|
||||||
|
return `
|
||||||
|
<div id="${escapeHtml(emptyStateId)}" 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" aria-hidden="true"></i>
|
||||||
|
</div>
|
||||||
|
<p class="text-sm font-semibold text-gray-700">${escapeHtml(title)}</p>
|
||||||
|
<p class="text-xs text-gray-500 mt-1 max-w-[220px] leading-relaxed">${escapeHtml(message)}</p>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderRecipeCard(recipe, { showSlotLabels = true, cardClassName = '' } = {}) {
|
||||||
|
const labels = showSlotLabels ? slotLabelsFor(recipe) : [];
|
||||||
|
const className = ['recipe-browser-card', cardClassName].filter(Boolean).join(' ');
|
||||||
|
|
||||||
|
return `
|
||||||
|
<button type="button" data-recipe-id="${escapeHtml(recipe.id)}" class="${className} rounded-xl overflow-hidden flex flex-col bg-[#393937] cursor-pointer text-left transition-shadow" style="background:#393937 !important; border:none !important; box-shadow:0 2px 8px rgba(0,0,0,0.28) !important;">
|
||||||
|
<div class="recipe-browser-card-media 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="recipe-browser-card-body p-3 flex flex-col flex-1">
|
||||||
|
<h3 class="recipe-browser-card-title text-sm font-medium underline decoration-1 underline-offset-2 text-[#f1ede4] mb-3 line-clamp-2">${escapeHtml(recipe.title)}</h3>
|
||||||
|
<div class="recipe-browser-card-footer mt-auto">
|
||||||
|
<div class="recipe-browser-card-meta 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]" aria-hidden="true"></i><span>${recipe.minutes} min</span></div>
|
||||||
|
<div class="flex items-center gap-1"><i class="fas fa-fire text-[#8f8b84]" aria-hidden="true"></i><span>${recipe.nutritionPerServing.kcal} kcal</span></div>
|
||||||
|
</div>
|
||||||
|
${labels.length > 0
|
||||||
|
? `<div class="recipe-browser-card-labels flex flex-wrap gap-1">
|
||||||
|
${labels.map((label) => `<span class="recipe-browser-card-label px-2 py-0.5 bg-[#2f2f2d] text-[#d7d2c8] text-[10px] rounded-md font-medium">${escapeHtml(label)}</span>`).join('')}
|
||||||
|
</div>`
|
||||||
|
: ''}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function filterRecipesByQuery(recipes, query = '') {
|
||||||
|
const q = query.trim().toLowerCase();
|
||||||
|
if (!q) return [...recipes];
|
||||||
|
|
||||||
|
return recipes.filter((recipe) => {
|
||||||
|
const haystack = `${recipe.title} ${(recipe.tags || []).join(' ')}`.toLowerCase();
|
||||||
|
return haystack.includes(q);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getRecipeGridSectionHTML({
|
||||||
|
scrollId,
|
||||||
|
gridId,
|
||||||
|
emptyStateId,
|
||||||
|
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',
|
||||||
|
} = {}) {
|
||||||
|
return `
|
||||||
|
<div id="${escapeHtml(scrollId)}" class="${scrollClassName}" style="background:#2d2e2b !important;">
|
||||||
|
<div id="${escapeHtml(gridId)}" class="${gridClassName}" style="background:#2d2e2b !important;"></div>
|
||||||
|
${getEmptyStateHTML({
|
||||||
|
emptyStateId,
|
||||||
|
title: emptyTitle,
|
||||||
|
message: emptyMessage,
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function renderRecipeGrid({
|
||||||
|
gridEl,
|
||||||
|
emptyStateEl,
|
||||||
|
recipes,
|
||||||
|
showSlotLabels = true,
|
||||||
|
cardClassName = '',
|
||||||
|
} = {}) {
|
||||||
|
if (!gridEl || !emptyStateEl) return;
|
||||||
|
|
||||||
|
const items = Array.isArray(recipes) ? recipes : [];
|
||||||
|
gridEl.innerHTML = items
|
||||||
|
.map((recipe) => renderRecipeCard(recipe, { showSlotLabels, cardClassName }))
|
||||||
|
.join('');
|
||||||
|
|
||||||
|
const hasItems = items.length > 0;
|
||||||
|
gridEl.classList.toggle('hidden', !hasItems);
|
||||||
|
emptyStateEl.classList.toggle('hidden', hasItems);
|
||||||
|
}
|
||||||
45
js/ui/recipeSearchField.js
Normal file
45
js/ui/recipeSearchField.js
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
export const RECIPE_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), inset 0 2px 6px rgba(0,0,0,0.16), inset 0 -1px 2px rgba(255,255,255,0.02)';
|
||||||
|
|
||||||
|
function escapeHtml(s) {
|
||||||
|
return String(s)
|
||||||
|
.replace(/&/g, '&')
|
||||||
|
.replace(/</g, '<')
|
||||||
|
.replace(/>/g, '>')
|
||||||
|
.replace(/"/g, '"');
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getRecipeSearchFieldHTML({
|
||||||
|
shellId,
|
||||||
|
inputId,
|
||||||
|
placeholder = 'Szukaj przepisów...',
|
||||||
|
inputAriaLabel = '',
|
||||||
|
inputValue = '',
|
||||||
|
filterButtonId = '',
|
||||||
|
filterButtonAction = '',
|
||||||
|
filterButtonLabel = 'Otwórz filtry',
|
||||||
|
} = {}) {
|
||||||
|
const hasFilterButton = Boolean(filterButtonId);
|
||||||
|
const actionAttr = hasFilterButton && filterButtonAction
|
||||||
|
? ` onclick="${escapeHtml(filterButtonAction)}"`
|
||||||
|
: '';
|
||||||
|
const inputPadding = hasFilterButton ? 'pl-8 pr-14' : 'pl-8 pr-8';
|
||||||
|
const ariaLabel = inputAriaLabel || placeholder;
|
||||||
|
|
||||||
|
return `
|
||||||
|
<div id="${escapeHtml(shellId)}" class="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:${RECIPE_SEARCH_SHELL_BASE_SHADOW} !important; transition:box-shadow 180ms ease;">
|
||||||
|
<input type="text" id="${escapeHtml(inputId)}" value="${escapeHtml(inputValue)}" placeholder="${escapeHtml(placeholder)}" aria-label="${escapeHtml(ariaLabel)}" class="w-full bg-transparent outline-none text-[15px] text-center py-[12px] ${inputPadding}" style="background:transparent !important; border:none !important; box-shadow:none !important; backdrop-filter:none !important;">
|
||||||
|
${hasFilterButton
|
||||||
|
? `
|
||||||
|
<button id="${escapeHtml(filterButtonId)}"${actionAttr} 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="${escapeHtml(filterButtonLabel)}">
|
||||||
|
<i class="fas fa-sliders-h" aria-hidden="true"></i>
|
||||||
|
</button>`
|
||||||
|
: ''}
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function syncRecipeSearchShellShadow(searchShell) {
|
||||||
|
if (!searchShell) return;
|
||||||
|
searchShell.style.boxShadow = RECIPE_SEARCH_SHELL_BASE_SHADOW;
|
||||||
|
}
|
||||||
@@ -20,6 +20,27 @@ const PREP_TIME_MAX = 120;
|
|||||||
const PREP_TIME_STEP = 5;
|
const PREP_TIME_STEP = 5;
|
||||||
const PREP_TIME_MIN_GAP = PREP_TIME_STEP;
|
const PREP_TIME_MIN_GAP = PREP_TIME_STEP;
|
||||||
const FILTER_RECIPE_BLUR = 'blur(3px) saturate(0.94)';
|
const FILTER_RECIPE_BLUR = 'blur(3px) saturate(0.94)';
|
||||||
|
const FILTER_CONTEXTS = {
|
||||||
|
recipes: {
|
||||||
|
anchorShellId: 'recipe-search-shell',
|
||||||
|
buttonId: 'recipe-filter-btn',
|
||||||
|
getState: () => getFilterState(),
|
||||||
|
applyState: (nextState) => applyFilters(nextState),
|
||||||
|
showSlots: true,
|
||||||
|
},
|
||||||
|
plannerPicker: {
|
||||||
|
anchorShellId: 'planner-picker-search-shell',
|
||||||
|
buttonId: 'planner-picker-filter-btn',
|
||||||
|
getState: () => window.getPlannerPickerFilterState?.() || ({
|
||||||
|
slots: [],
|
||||||
|
tags: [],
|
||||||
|
minMinutes: PREP_TIME_MIN,
|
||||||
|
maxMinutes: PREP_TIME_MAX,
|
||||||
|
}),
|
||||||
|
applyState: (nextState) => window.applyPlannerPickerFilters?.(nextState),
|
||||||
|
showSlots: false,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
function escapeHtml(s) {
|
function escapeHtml(s) {
|
||||||
return String(s)
|
return String(s)
|
||||||
@@ -61,7 +82,7 @@ export function getFilterHTML() {
|
|||||||
outline: none;
|
outline: none;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
<div id="filter-view" class="absolute inset-0 z-[55] hidden opacity-0 transition-opacity duration-150" style="pointer-events:none; background:rgba(0,0,0,0.5) !important; background-image:none !important;" aria-hidden="true">
|
<div id="filter-view" class="absolute inset-0 z-[70] hidden opacity-0 transition-opacity duration-150" style="pointer-events:none; background:rgba(0,0,0,0.5) !important; background-image:none !important;" aria-hidden="true">
|
||||||
<div id="filter-panel" class="absolute flex flex-col overflow-hidden rounded-[1.5rem] border" style="background:${FILTER_SURFACE} !important; background-image:none !important; border-color:${FILTER_BORDER} !important; opacity:0; transform:translateY(-0.5rem) scale(0.98); transform-origin:top center; transition:${FILTER_PANEL_TRANSITION}; box-shadow:0 18px 40px rgba(0,0,0,0.34), 0 4px 12px rgba(0,0,0,0.18); width:min(calc(100% - 1.5rem), 22rem);">
|
<div id="filter-panel" class="absolute flex flex-col overflow-hidden rounded-[1.5rem] border" style="background:${FILTER_SURFACE} !important; background-image:none !important; border-color:${FILTER_BORDER} !important; opacity:0; transform:translateY(-0.5rem) scale(0.98); transform-origin:top center; transition:${FILTER_PANEL_TRANSITION}; box-shadow:0 18px 40px rgba(0,0,0,0.34), 0 4px 12px rgba(0,0,0,0.18); width:min(calc(100% - 1.5rem), 22rem);">
|
||||||
<div class="pointer-events-none absolute inset-x-0 top-0 h-px" style="background:rgba(242,239,232,0.12);" aria-hidden="true"></div>
|
<div class="pointer-events-none absolute inset-x-0 top-0 h-px" style="background:rgba(242,239,232,0.12);" aria-hidden="true"></div>
|
||||||
<div class="shrink-0 px-3.5 pt-3 pb-2 flex justify-end" style="background:${FILTER_SURFACE} !important; background-image:none !important;">
|
<div class="shrink-0 px-3.5 pt-3 pb-2 flex justify-end" style="background:${FILTER_SURFACE} !important; background-image:none !important;">
|
||||||
@@ -71,7 +92,7 @@ export function getFilterHTML() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="filter-panel-body" class="min-h-0 flex-1 overflow-y-auto no-scrollbar px-4 pb-4 space-y-2.5" style="background:${FILTER_SURFACE} !important; background-image:none !important;">
|
<div id="filter-panel-body" class="min-h-0 flex-1 overflow-y-auto no-scrollbar px-4 pb-4 space-y-2.5" style="background:${FILTER_SURFACE} !important; background-image:none !important;">
|
||||||
<section class="p-3.5" style="background:${FILTER_SURFACE} !important; background-image:none !important;">
|
<section id="filter-slot-section" class="p-3.5" style="background:${FILTER_SURFACE} !important; background-image:none !important;">
|
||||||
<p class="text-[10px] font-bold uppercase tracking-wider mb-3" style="color:${FILTER_TEXT_MUTED};">Pora posiłku</p>
|
<p class="text-[10px] font-bold uppercase tracking-wider mb-3" style="color:${FILTER_TEXT_MUTED};">Pora posiłku</p>
|
||||||
<div id="filter-slot-chips" class="flex flex-wrap gap-2"></div>
|
<div id="filter-slot-chips" class="flex flex-wrap gap-2"></div>
|
||||||
</section>
|
</section>
|
||||||
@@ -132,6 +153,11 @@ let localTags = [];
|
|||||||
let localMinMinutes = PREP_TIME_MIN;
|
let localMinMinutes = PREP_TIME_MIN;
|
||||||
let localMaxMinutes = PREP_TIME_MAX;
|
let localMaxMinutes = PREP_TIME_MAX;
|
||||||
let closeTimer = null;
|
let closeTimer = null;
|
||||||
|
let activeFilterContext = 'recipes';
|
||||||
|
|
||||||
|
function getActiveFilterConfig() {
|
||||||
|
return FILTER_CONTEXTS[activeFilterContext] || FILTER_CONTEXTS.recipes;
|
||||||
|
}
|
||||||
|
|
||||||
function normalizeTimeRange(minMinutes, maxMinutes) {
|
function normalizeTimeRange(minMinutes, maxMinutes) {
|
||||||
let nextMin = snapTimeValue(minMinutes);
|
let nextMin = snapTimeValue(minMinutes);
|
||||||
@@ -216,9 +242,10 @@ function positionFilterPanel() {
|
|||||||
const view = document.getElementById('filter-view');
|
const view = document.getElementById('filter-view');
|
||||||
const panel = document.getElementById('filter-panel');
|
const panel = document.getElementById('filter-panel');
|
||||||
const body = document.getElementById('filter-panel-body');
|
const body = document.getElementById('filter-panel-body');
|
||||||
const searchShell = document.getElementById('recipe-search-shell');
|
const { anchorShellId, buttonId } = getActiveFilterConfig();
|
||||||
const button = document.getElementById('recipe-filter-btn');
|
const searchShell = document.getElementById(anchorShellId);
|
||||||
if (!view || !panel || !button) return;
|
const button = document.getElementById(buttonId);
|
||||||
|
if (!view || !panel || (!searchShell && !button)) return;
|
||||||
|
|
||||||
const viewRect = view.getBoundingClientRect();
|
const viewRect = view.getBoundingClientRect();
|
||||||
const anchorRect = (searchShell || button).getBoundingClientRect();
|
const anchorRect = (searchShell || button).getBoundingClientRect();
|
||||||
@@ -327,9 +354,16 @@ function renderTagChips() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function syncFilterSections() {
|
||||||
|
const slotSection = document.getElementById('filter-slot-section');
|
||||||
|
if (!slotSection) return;
|
||||||
|
slotSection.classList.toggle('hidden', !getActiveFilterConfig().showSlots);
|
||||||
|
}
|
||||||
|
|
||||||
function syncLiveFilters() {
|
function syncLiveFilters() {
|
||||||
applyFilters({
|
const config = getActiveFilterConfig();
|
||||||
slots: localSlots,
|
config.applyState?.({
|
||||||
|
slots: config.showSlots ? localSlots : [],
|
||||||
tags: localTags,
|
tags: localTags,
|
||||||
minMinutes: localMinMinutes,
|
minMinutes: localMinMinutes,
|
||||||
maxMinutes: localMaxMinutes,
|
maxMinutes: localMaxMinutes,
|
||||||
@@ -465,15 +499,17 @@ export function setupFilter() {
|
|||||||
syncLiveFilters();
|
syncLiveFilters();
|
||||||
});
|
});
|
||||||
|
|
||||||
window.openFilters = () => {
|
window.openFilters = (contextName = 'recipes') => {
|
||||||
if (isFilterPanelOpen()) {
|
if (isFilterPanelOpen() && activeFilterContext === contextName) {
|
||||||
window.closeFilters();
|
window.closeFilters();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const state = getFilterState();
|
activeFilterContext = FILTER_CONTEXTS[contextName] ? contextName : 'recipes';
|
||||||
localSlots = [...state.slots];
|
const config = getActiveFilterConfig();
|
||||||
localTags = [...state.tags];
|
const state = config.getState?.() || {};
|
||||||
|
localSlots = [...(state.slots || [])];
|
||||||
|
localTags = [...(state.tags || [])];
|
||||||
const normalized = normalizeTimeRange(
|
const normalized = normalizeTimeRange(
|
||||||
Number.isFinite(state.minMinutes) ? state.minMinutes : PREP_TIME_MIN,
|
Number.isFinite(state.minMinutes) ? state.minMinutes : PREP_TIME_MIN,
|
||||||
Number.isFinite(state.maxMinutes) ? state.maxMinutes : PREP_TIME_MAX,
|
Number.isFinite(state.maxMinutes) ? state.maxMinutes : PREP_TIME_MAX,
|
||||||
@@ -485,6 +521,7 @@ export function setupFilter() {
|
|||||||
|
|
||||||
renderSlotChips();
|
renderSlotChips();
|
||||||
renderTagChips();
|
renderTagChips();
|
||||||
|
syncFilterSections();
|
||||||
|
|
||||||
showFilterPanel();
|
showFilterPanel();
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -37,6 +37,11 @@ import {
|
|||||||
syncCalendarTodayButton,
|
syncCalendarTodayButton,
|
||||||
syncCollapsibleCalendarMode,
|
syncCollapsibleCalendarMode,
|
||||||
} from '../ui/mealCalendar.js?v=1';
|
} from '../ui/mealCalendar.js?v=1';
|
||||||
|
import {
|
||||||
|
filterRecipesByQuery,
|
||||||
|
renderRecipeGrid,
|
||||||
|
} from '../ui/recipeGrid.js';
|
||||||
|
import { getRecipeSearchFieldHTML } from '../ui/recipeSearchField.js';
|
||||||
|
|
||||||
const WEEKDAYS_LONG = [
|
const WEEKDAYS_LONG = [
|
||||||
'Niedziela', 'Poniedziałek', 'Wtorek', 'Środa', 'Czwartek', 'Piątek', 'Sobota',
|
'Niedziela', 'Poniedziałek', 'Wtorek', 'Środa', 'Czwartek', 'Piątek', 'Sobota',
|
||||||
@@ -45,6 +50,11 @@ const WEEKDAYS_LONG = [
|
|||||||
const PLANNER_SHEET_BOTTOM_INSET = '5.25rem';
|
const PLANNER_SHEET_BOTTOM_INSET = '5.25rem';
|
||||||
const PLANNER_SHEET_MAX_HEIGHT = '70vh';
|
const PLANNER_SHEET_MAX_HEIGHT = '70vh';
|
||||||
const PLANNER_SHEET_OFF_TRANSFORM = `translateY(calc(100% + ${PLANNER_SHEET_BOTTOM_INSET}))`;
|
const PLANNER_SHEET_OFF_TRANSFORM = `translateY(calc(100% + ${PLANNER_SHEET_BOTTOM_INSET}))`;
|
||||||
|
const PLANNER_PICKER_OFF_TRANSFORM = 'translateY(100%)';
|
||||||
|
const PLANNER_MEAL_SWIPE_MAX = 132;
|
||||||
|
const PLANNER_MEAL_SWIPE_DELETE_THRESHOLD = 96;
|
||||||
|
const PICKER_FILTER_MIN_MINUTES = 5;
|
||||||
|
const PICKER_FILTER_MAX_MINUTES = 120;
|
||||||
|
|
||||||
function recipesForSlot(slotId) {
|
function recipesForSlot(slotId) {
|
||||||
return Object.values(RECIPES).filter((r) => r.allowedSlots.includes(slotId));
|
return Object.values(RECIPES).filter((r) => r.allowedSlots.includes(slotId));
|
||||||
@@ -115,22 +125,40 @@ export function getMealPlannerHTML() {
|
|||||||
</button>
|
</button>
|
||||||
<div id="planner-meal-slots" class="space-y-3 pb-2 bg-[#2d2e2b]"></div>
|
<div id="planner-meal-slots" class="space-y-3 pb-2 bg-[#2d2e2b]"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="planner-picker-backdrop" class="absolute left-0 right-0 top-0 z-[45] bg-black/45 hidden opacity-0 transition-opacity duration-200" style="bottom: ${PLANNER_SHEET_BOTTOM_INSET}" aria-hidden="true"></div>
|
|
||||||
<div id="planner-picker-sheet" class="absolute left-0 right-0 z-[50] rounded-t-3xl shadow-[0_-10px_40px_rgba(0,0,0,0.12)] flex flex-col will-change-transform" style="visibility: hidden; bottom: ${PLANNER_SHEET_BOTTOM_INSET}; height: auto; max-height: ${PLANNER_SHEET_MAX_HEIGHT}; transform: ${PLANNER_SHEET_OFF_TRANSFORM}; transition: transform 300ms cubic-bezier(0.32, 0.72, 0, 1); background:#2d2e2b !important; background-image:none !important;" role="dialog" aria-labelledby="planner-picker-title" aria-modal="true">
|
|
||||||
<div class="shrink-0 px-4 pt-3 pb-2 border-b border-[#444442] touch-none cursor-grab active:cursor-grabbing select-none" data-planner-sheet-drag-zone aria-label="Przeciągnij w dół, by zamknąć">
|
|
||||||
<div class="w-10 h-1 bg-[#6d6c67]/75 rounded-full mx-auto mb-2.5" aria-hidden="true"></div>
|
|
||||||
<h2 id="planner-picker-title" class="text-[15px] font-bold text-[#ddd6ca] leading-tight pr-2">Wybierz przepis</h2>
|
|
||||||
<p id="planner-picker-sub" class="text-[11px] text-[#9b978f] mt-1"></p>
|
|
||||||
</div>
|
|
||||||
<div class="shrink-0 px-4 pt-2 pb-2">
|
|
||||||
<input type="text" id="planner-picker-search" class="w-full rounded-xl border border-[#444442] bg-[#2f2f2d] px-3 py-2 text-sm text-[#ddd6ca] outline-none focus:border-[#6d6c67] placeholder:text-[#7d7a74]" placeholder="Szukaj przepisu…" />
|
|
||||||
</div>
|
|
||||||
<div id="planner-picker-list" class="min-h-0 flex-1 overflow-y-auto no-scrollbar px-4 py-2.5 pb-8 space-y-2"></div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="planner-ing-backdrop" class="absolute left-0 right-0 top-0 z-[45] bg-black/45 hidden opacity-0 transition-opacity duration-200" style="bottom: ${PLANNER_SHEET_BOTTOM_INSET}" aria-hidden="true"></div>
|
<div id="planner-picker-backdrop" class="absolute inset-0 z-[55] bg-black/45 hidden opacity-0 transition-opacity duration-200" aria-hidden="true"></div>
|
||||||
<div id="planner-ing-sheet" class="absolute left-0 right-0 z-[50] rounded-t-3xl shadow-[0_-10px_40px_rgba(0,0,0,0.12)] flex flex-col will-change-transform" style="visibility: hidden; bottom: ${PLANNER_SHEET_BOTTOM_INSET}; height: auto; max-height: ${PLANNER_SHEET_MAX_HEIGHT}; transform: ${PLANNER_SHEET_OFF_TRANSFORM}; transition: transform 300ms cubic-bezier(0.32, 0.72, 0, 1); background:#2d2e2b !important; background-image:none !important;" role="dialog" aria-labelledby="planner-ing-title" aria-modal="true">
|
<div id="planner-picker-sheet" data-off-transform="${PLANNER_PICKER_OFF_TRANSFORM}" class="absolute inset-x-0 bottom-0 z-[60] flex flex-col will-change-transform rounded-t-[1.85rem] overflow-hidden" style="top:calc(env(safe-area-inset-top) + 0.35rem); visibility: hidden; transform: ${PLANNER_PICKER_OFF_TRANSFORM}; transition: transform 300ms cubic-bezier(0.32, 0.72, 0, 1); background:#2d2e2b !important; background-image:none !important;" role="dialog" aria-label="Wybierz przepis" aria-modal="true">
|
||||||
|
<div class="pointer-events-none absolute inset-x-0 top-0 z-[2] px-4 pt-3">
|
||||||
|
<div class="pointer-events-auto pb-4 touch-none cursor-grab active:cursor-grabbing select-none" data-planner-sheet-drag-zone aria-label="Przeciągnij w dół, by zamknąć">
|
||||||
|
<div class="w-10 h-1 bg-[#6d6c67]/75 rounded-full mx-auto" aria-hidden="true"></div>
|
||||||
|
</div>
|
||||||
|
<div class="pointer-events-auto">
|
||||||
|
${getRecipeSearchFieldHTML({
|
||||||
|
shellId: 'planner-picker-search-shell',
|
||||||
|
inputId: 'planner-picker-search',
|
||||||
|
placeholder: 'Wybierz przepis',
|
||||||
|
inputAriaLabel: 'Wybierz przepis',
|
||||||
|
filterButtonId: 'planner-picker-filter-btn',
|
||||||
|
filterButtonAction: 'openFilters(\'plannerPicker\')',
|
||||||
|
filterButtonLabel: 'Otwórz filtry',
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div id="planner-picker-scroll" class="relative min-h-0 flex-1 overflow-y-auto px-4 pt-28 pb-8 bg-[#2d2e2b]" style="background:#2d2e2b !important;">
|
||||||
|
<div id="planner-picker-grid" class="grid grid-cols-3 gap-2 bg-[#2d2e2b]" style="background:#2d2e2b !important;"></div>
|
||||||
|
<div id="planner-picker-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" aria-hidden="true"></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">Spróbuj innej nazwy przepisu</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="planner-ing-backdrop" class="absolute inset-0 z-[55] bg-black/45 hidden opacity-0 transition-opacity duration-200" aria-hidden="true"></div>
|
||||||
|
<div id="planner-ing-sheet" class="absolute left-0 right-0 bottom-0 z-[60] rounded-t-3xl shadow-[0_-10px_40px_rgba(0,0,0,0.12)] flex flex-col will-change-transform" style="visibility: hidden; height: auto; max-height: calc(100vh - env(safe-area-inset-top) - 1rem); transform: ${PLANNER_SHEET_OFF_TRANSFORM}; transition: transform 300ms cubic-bezier(0.32, 0.72, 0, 1); background:#2d2e2b !important; background-image:none !important;" role="dialog" aria-labelledby="planner-ing-title" aria-modal="true">
|
||||||
<div class="shrink-0 px-4 pt-3 pb-2 border-b border-[#444442] touch-none cursor-grab active:cursor-grabbing select-none" data-planner-sheet-drag-zone aria-label="Przeciągnij w dół, by zamknąć">
|
<div class="shrink-0 px-4 pt-3 pb-2 border-b border-[#444442] touch-none cursor-grab active:cursor-grabbing select-none" data-planner-sheet-drag-zone aria-label="Przeciągnij w dół, by zamknąć">
|
||||||
<div class="w-10 h-1 bg-[#6d6c67]/75 rounded-full mx-auto mb-2.5" aria-hidden="true"></div>
|
<div class="w-10 h-1 bg-[#6d6c67]/75 rounded-full mx-auto mb-2.5" aria-hidden="true"></div>
|
||||||
<h2 id="planner-ing-title" class="text-[15px] font-bold text-[#ddd6ca] leading-tight pr-2">Składniki i spiżarnia</h2>
|
<h2 id="planner-ing-title" class="text-[15px] font-bold text-[#ddd6ca] leading-tight pr-2">Składniki i spiżarnia</h2>
|
||||||
@@ -149,10 +177,9 @@ export function getMealPlannerHTML() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="planner-toast" class="pointer-events-none absolute left-4 right-4 bottom-28 z-[55] opacity-0 translate-y-2 transition-all duration-300" role="status">
|
<div id="planner-toast" class="pointer-events-none absolute left-4 right-4 bottom-28 z-[65] opacity-0 translate-y-2 transition-all duration-300" role="status">
|
||||||
<div class="rounded-xl bg-gray-900 text-white text-sm font-medium px-4 py-3 shadow-lg text-center" id="planner-toast-text"></div>
|
<div class="rounded-xl bg-gray-900 text-white text-sm font-medium px-4 py-3 shadow-lg text-center" id="planner-toast-text"></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -254,10 +281,14 @@ function openSheet(backdrop, sheet) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getSheetOffTransform(sheet) {
|
||||||
|
return sheet?.dataset.offTransform || PLANNER_SHEET_OFF_TRANSFORM;
|
||||||
|
}
|
||||||
|
|
||||||
function closeSheet(backdrop, sheet) {
|
function closeSheet(backdrop, sheet) {
|
||||||
if (!backdrop || !sheet) return;
|
if (!backdrop || !sheet) return;
|
||||||
sheet.style.transition = 'transform 300ms cubic-bezier(0.32, 0.72, 0, 1)';
|
sheet.style.transition = 'transform 300ms cubic-bezier(0.32, 0.72, 0, 1)';
|
||||||
sheet.style.transform = PLANNER_SHEET_OFF_TRANSFORM;
|
sheet.style.transform = getSheetOffTransform(sheet);
|
||||||
backdrop.classList.add('opacity-0');
|
backdrop.classList.add('opacity-0');
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
backdrop.classList.add('hidden');
|
backdrop.classList.add('hidden');
|
||||||
@@ -265,60 +296,117 @@ function closeSheet(backdrop, sheet) {
|
|||||||
}, 300);
|
}, 300);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Zamykanie panelu: przeciągnięcie nagłówka w dół (pointer). */
|
/** Zamykanie panelu: przeciągnięcie w dół (pointer). */
|
||||||
function bindPlannerSheetDragClose(sheet, closeFn) {
|
function bindPlannerSheetDragClose(sheet, closeFn, options = {}) {
|
||||||
const zone = sheet.querySelector('[data-planner-sheet-drag-zone]');
|
const zone = options.dragSurface || sheet.querySelector('[data-planner-sheet-drag-zone]');
|
||||||
|
const scrollEl = options.scrollEl || null;
|
||||||
if (!zone || !sheet) return;
|
if (!zone || !sheet) return;
|
||||||
|
|
||||||
|
let startX = 0;
|
||||||
let startY = 0;
|
let startY = 0;
|
||||||
let pulling = false;
|
|
||||||
let ptrId = null;
|
let ptrId = null;
|
||||||
|
let engaged = false;
|
||||||
|
let startFromHandle = false;
|
||||||
|
let suppressClickUntil = 0;
|
||||||
|
|
||||||
const resetVisual = () => {
|
const resetVisual = () => {
|
||||||
sheet.style.transition = 'transform 300ms cubic-bezier(0.32, 0.72, 0, 1)';
|
sheet.style.transition = 'transform 300ms cubic-bezier(0.32, 0.72, 0, 1)';
|
||||||
sheet.style.transform = 'translateY(0)';
|
sheet.style.transform = 'translateY(0)';
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const stopTracking = () => {
|
||||||
|
window.removeEventListener('pointermove', onPointerMove);
|
||||||
|
window.removeEventListener('pointerup', onPointerUp);
|
||||||
|
window.removeEventListener('pointercancel', onPointerCancel);
|
||||||
|
ptrId = null;
|
||||||
|
engaged = false;
|
||||||
|
startFromHandle = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
const shouldIgnoreTarget = (target) => target instanceof Element
|
||||||
|
&& Boolean(target.closest('input, textarea, select, [contenteditable="true"]'));
|
||||||
|
|
||||||
zone.addEventListener('pointerdown', (e) => {
|
zone.addEventListener('pointerdown', (e) => {
|
||||||
if (e.pointerType === 'mouse' && e.button !== 0) return;
|
if (e.pointerType === 'mouse' && e.button !== 0) return;
|
||||||
pulling = true;
|
if (shouldIgnoreTarget(e.target)) return;
|
||||||
ptrId = e.pointerId;
|
|
||||||
startY = e.clientY;
|
|
||||||
sheet.style.transition = 'none';
|
|
||||||
zone.setPointerCapture(e.pointerId);
|
|
||||||
});
|
|
||||||
|
|
||||||
zone.addEventListener('pointermove', (e) => {
|
if (scrollEl && scrollEl.scrollTop > 0 && !(e.target instanceof Element && e.target.closest('[data-planner-sheet-drag-zone]'))) {
|
||||||
if (!pulling || e.pointerId !== ptrId) return;
|
return;
|
||||||
const dy = Math.max(0, e.clientY - startY);
|
|
||||||
sheet.style.transform = `translateY(${dy}px)`;
|
|
||||||
});
|
|
||||||
|
|
||||||
zone.addEventListener('pointerup', (e) => {
|
|
||||||
if (!pulling || e.pointerId !== ptrId) return;
|
|
||||||
const dy = e.clientY - startY;
|
|
||||||
pulling = false;
|
|
||||||
ptrId = null;
|
|
||||||
try {
|
|
||||||
zone.releasePointerCapture(e.pointerId);
|
|
||||||
} catch {
|
|
||||||
/* ignore */
|
|
||||||
}
|
}
|
||||||
if (dy > 56) {
|
|
||||||
|
ptrId = e.pointerId;
|
||||||
|
startX = e.clientX;
|
||||||
|
startY = e.clientY;
|
||||||
|
engaged = false;
|
||||||
|
startFromHandle = e.target instanceof Element && Boolean(e.target.closest('[data-planner-sheet-drag-zone]'));
|
||||||
|
|
||||||
|
window.addEventListener('pointermove', onPointerMove);
|
||||||
|
window.addEventListener('pointerup', onPointerUp);
|
||||||
|
window.addEventListener('pointercancel', onPointerCancel);
|
||||||
|
});
|
||||||
|
|
||||||
|
const onPointerMove = (e) => {
|
||||||
|
if (e.pointerId !== ptrId) return;
|
||||||
|
|
||||||
|
const dx = e.clientX - startX;
|
||||||
|
const rawDy = e.clientY - startY;
|
||||||
|
|
||||||
|
if (!engaged) {
|
||||||
|
if (Math.abs(dx) < 8 && Math.abs(rawDy) < 8) return;
|
||||||
|
|
||||||
|
if (rawDy <= 0 || Math.abs(dx) > Math.abs(rawDy)) {
|
||||||
|
stopTracking();
|
||||||
|
resetVisual();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!startFromHandle && scrollEl && scrollEl.scrollTop > 0) {
|
||||||
|
stopTracking();
|
||||||
|
resetVisual();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
engaged = true;
|
||||||
|
suppressClickUntil = Date.now() + 250;
|
||||||
|
sheet.style.transition = 'none';
|
||||||
|
}
|
||||||
|
|
||||||
|
const dy = Math.max(0, rawDy);
|
||||||
|
sheet.style.transform = `translateY(${dy}px)`;
|
||||||
|
if (e.cancelable) e.preventDefault();
|
||||||
|
};
|
||||||
|
|
||||||
|
const onPointerUp = (e) => {
|
||||||
|
if (e.pointerId !== ptrId) return;
|
||||||
|
const dy = e.clientY - startY;
|
||||||
|
const shouldClose = engaged && dy > 56;
|
||||||
|
stopTracking();
|
||||||
|
if (shouldClose) {
|
||||||
closeFn();
|
closeFn();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
resetVisual();
|
resetVisual();
|
||||||
});
|
};
|
||||||
|
|
||||||
zone.addEventListener('pointercancel', () => {
|
const onPointerCancel = (e) => {
|
||||||
pulling = false;
|
if (ptrId !== null && e.pointerId !== ptrId) return;
|
||||||
ptrId = null;
|
stopTracking();
|
||||||
resetVisual();
|
resetVisual();
|
||||||
|
};
|
||||||
|
|
||||||
|
zone.addEventListener('click', (e) => {
|
||||||
|
if (Date.now() < suppressClickUntil) {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
}
|
||||||
|
}, true);
|
||||||
|
|
||||||
|
zone.addEventListener('pointercancel', (e) => {
|
||||||
|
onPointerCancel(e);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderDayContent(state) {
|
function renderDayContent(state, onMealRemoved = null) {
|
||||||
const sel = state.selected;
|
const sel = state.selected;
|
||||||
const dayPlan = getDayPlan(state.plans, sel);
|
const dayPlan = getDayPlan(state.plans, sel);
|
||||||
const totals = sumDayNutrition(dayPlan);
|
const totals = sumDayNutrition(dayPlan);
|
||||||
@@ -388,7 +476,14 @@ function renderDayContent(state) {
|
|||||||
const customDot = hasCustom ? '<span class="w-1.5 h-1.5 rounded-full bg-amber-400 inline-block shrink-0 ml-1"></span>' : '';
|
const customDot = hasCustom ? '<span class="w-1.5 h-1.5 rounded-full bg-amber-400 inline-block shrink-0 ml-1"></span>' : '';
|
||||||
const servLabel = servings > 1 ? `<span class="mx-1.5 text-[#6d6c67]">·</span>×${servings}` : '';
|
const servLabel = servings > 1 ? `<span class="mx-1.5 text-[#6d6c67]">·</span>×${servings}` : '';
|
||||||
return `
|
return `
|
||||||
<div class="rounded-lg bg-[#2d2e2b] p-2" style="box-shadow:inset 0 1px 3px rgba(0,0,0,0.3);" data-slot-id="${slot.id}" data-entry-id="${eid}">
|
<div class="relative overflow-hidden rounded-lg" data-planner-swipe-row style="--planner-swipe-progress:0;" data-slot-id="${slot.id}" data-entry-id="${eid}">
|
||||||
|
<div class="pointer-events-none absolute inset-0 flex items-center justify-end px-4" style="background:rgba(203,74,72, calc(0.18 + var(--planner-swipe-progress) * 0.5));">
|
||||||
|
<span class="inline-flex items-center gap-1.5 text-[11px] font-semibold tracking-wide uppercase" style="color:rgba(250,234,234, calc(0.55 + var(--planner-swipe-progress) * 0.45));">
|
||||||
|
<i class="fas fa-trash text-[10px]" aria-hidden="true"></i>
|
||||||
|
Usuń
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="relative z-[1] rounded-lg bg-[#2d2e2b] p-2" style="box-shadow:inset 0 1px 3px rgba(0,0,0,0.3); transform:translateX(0); transition:transform 180ms cubic-bezier(0.22, 1, 0.36, 1), opacity 180ms ease; touch-action:pan-y;" data-planner-swipe-card data-slot-id="${slot.id}" data-entry-id="${eid}">
|
||||||
<div class="flex items-start justify-between gap-2">
|
<div class="flex items-start justify-between gap-2">
|
||||||
<div class="flex items-center gap-2 min-w-0 cursor-pointer planner-open-recipe" data-recipe-id="${escapeHtml(recipe.id)}">
|
<div class="flex items-center gap-2 min-w-0 cursor-pointer planner-open-recipe" data-recipe-id="${escapeHtml(recipe.id)}">
|
||||||
<div class="w-8 h-8 rounded-lg bg-[#3a3a37] overflow-hidden shrink-0">
|
<div class="w-8 h-8 rounded-lg bg-[#3a3a37] overflow-hidden shrink-0">
|
||||||
@@ -397,7 +492,7 @@ function renderDayContent(state) {
|
|||||||
: `<span class="w-full h-full flex items-center justify-center text-white text-[8px] font-medium">${escapeHtml(recipe.thumbLabel)}</span>`}
|
: `<span class="w-full h-full flex items-center justify-center text-white text-[8px] font-medium">${escapeHtml(recipe.thumbLabel)}</span>`}
|
||||||
</div>
|
</div>
|
||||||
<div class="min-w-0">
|
<div class="min-w-0">
|
||||||
<div class="flex items-center"><p class="text-[13px] font-bold text-[#ddd6ca] truncate underline decoration-1 underline-offset-2">${escapeHtml(recipe.title)}</p>${customDot}</div>
|
<div class="flex items-center"><p class="text-[13px] font-normal text-[#ddd6ca] truncate">${escapeHtml(recipe.title)}</p>${customDot}</div>
|
||||||
<p class="text-[11px] text-[#9b978f] mt-0.5 tabular-nums">
|
<p class="text-[11px] text-[#9b978f] mt-0.5 tabular-nums">
|
||||||
<i class="fas fa-clock text-[#7d7a74] mr-0.5" aria-hidden="true"></i>${recipe.minutes} min
|
<i class="fas fa-clock text-[#7d7a74] mr-0.5" aria-hidden="true"></i>${recipe.minutes} min
|
||||||
<span class="mx-1.5 text-[#6d6c67]">·</span>
|
<span class="mx-1.5 text-[#6d6c67]">·</span>
|
||||||
@@ -405,13 +500,11 @@ function renderDayContent(state) {
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center gap-1 shrink-0">
|
<div class="flex items-center gap-1 shrink-0 self-center">
|
||||||
<button type="button" class="planner-edit-meal w-6 h-6 rounded-full border border-[#444442] text-[#9b978f] hover:text-[#ddd6ca] hover:border-[#6d6c67] hover:bg-[#3a3a37] flex items-center justify-center transition-colors" data-slot-id="${slot.id}" data-entry-id="${eid}" aria-label="Edytuj ten przepis">
|
<button type="button" class="planner-edit-meal w-6 h-6 rounded-full border border-[#444442] text-[#9b978f] hover:text-[#ddd6ca] hover:border-[#6d6c67] hover:bg-[#3a3a37] flex items-center justify-center transition-colors" data-slot-id="${slot.id}" data-entry-id="${eid}" aria-label="Edytuj ten przepis">
|
||||||
<i class="fas fa-pencil text-[9px]" aria-hidden="true"></i>
|
<i class="fas fa-pencil text-[9px]" aria-hidden="true"></i>
|
||||||
</button>
|
</button>
|
||||||
<button type="button" class="planner-clear-meal w-6 h-6 rounded-full border border-[#444442] text-[#9b978f] hover:text-red-400 hover:border-red-300/60 hover:bg-[#3a2326] transition-colors flex items-center justify-center" data-slot-id="${slot.id}" data-entry-id="${eid}" aria-label="Usuń ten przepis">
|
</div>
|
||||||
<i class="fas fa-times text-[9px]" aria-hidden="true"></i>
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>`;
|
</div>`;
|
||||||
@@ -450,6 +543,147 @@ function renderDayContent(state) {
|
|||||||
</div>
|
</div>
|
||||||
</div>`;
|
</div>`;
|
||||||
}).join('');
|
}).join('');
|
||||||
|
|
||||||
|
bindMealEntrySwipe(state, onMealRemoved);
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeMealEntry(state, slotId, entryId) {
|
||||||
|
const key = dateKey(state.selected);
|
||||||
|
const day = state.plans[key];
|
||||||
|
const arr = day?.[slotId];
|
||||||
|
if (!Array.isArray(arr) || !entryId) return false;
|
||||||
|
|
||||||
|
const next = arr.filter((x) => x && x.id !== entryId);
|
||||||
|
if (next.length === arr.length) return false;
|
||||||
|
|
||||||
|
if (next.length === 0) delete day[slotId];
|
||||||
|
else day[slotId] = next;
|
||||||
|
|
||||||
|
if (Object.keys(day).length === 0) delete state.plans[key];
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function bindMealEntrySwipe(state, onMealRemoved = null) {
|
||||||
|
const cards = document.querySelectorAll('[data-planner-swipe-card]');
|
||||||
|
cards.forEach((card) => {
|
||||||
|
const row = card.closest('[data-planner-swipe-row]');
|
||||||
|
if (!row) return;
|
||||||
|
|
||||||
|
let pointerId = null;
|
||||||
|
let startX = 0;
|
||||||
|
let startY = 0;
|
||||||
|
let dx = 0;
|
||||||
|
let engaged = false;
|
||||||
|
let suppressClickUntil = 0;
|
||||||
|
|
||||||
|
const resetVisual = () => {
|
||||||
|
row.classList.remove('planner-meal-row-swiping');
|
||||||
|
row.style.setProperty('--planner-swipe-progress', '0');
|
||||||
|
row.style.pointerEvents = '';
|
||||||
|
card.style.willChange = '';
|
||||||
|
card.style.transition = 'transform 180ms cubic-bezier(0.22, 1, 0.36, 1), opacity 180ms ease';
|
||||||
|
card.style.transform = 'translateX(0)';
|
||||||
|
card.style.opacity = '1';
|
||||||
|
};
|
||||||
|
|
||||||
|
const stopTracking = () => {
|
||||||
|
window.removeEventListener('pointermove', onPointerMove);
|
||||||
|
window.removeEventListener('pointerup', onPointerUp);
|
||||||
|
window.removeEventListener('pointercancel', onPointerCancel);
|
||||||
|
pointerId = null;
|
||||||
|
};
|
||||||
|
|
||||||
|
const onPointerMove = (e) => {
|
||||||
|
if (e.pointerId !== pointerId) return;
|
||||||
|
|
||||||
|
dx = e.clientX - startX;
|
||||||
|
const dy = e.clientY - startY;
|
||||||
|
|
||||||
|
if (!engaged) {
|
||||||
|
if (Math.abs(dx) < 8 && Math.abs(dy) < 8) return;
|
||||||
|
suppressClickUntil = Date.now() + 250;
|
||||||
|
if (Math.abs(dy) > Math.abs(dx) || dx >= 0) {
|
||||||
|
stopTracking();
|
||||||
|
resetVisual();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
engaged = true;
|
||||||
|
row.classList.add('planner-meal-row-swiping');
|
||||||
|
card.style.transition = 'none';
|
||||||
|
card.style.willChange = 'transform';
|
||||||
|
}
|
||||||
|
|
||||||
|
const translateX = Math.max(-PLANNER_MEAL_SWIPE_MAX, Math.min(0, dx));
|
||||||
|
const progress = Math.min(1, Math.abs(translateX) / PLANNER_MEAL_SWIPE_DELETE_THRESHOLD);
|
||||||
|
row.style.setProperty('--planner-swipe-progress', String(progress));
|
||||||
|
card.style.transform = `translateX(${translateX}px)`;
|
||||||
|
|
||||||
|
if (e.cancelable) e.preventDefault();
|
||||||
|
};
|
||||||
|
|
||||||
|
const onPointerUp = (e) => {
|
||||||
|
if (e.pointerId !== pointerId) return;
|
||||||
|
|
||||||
|
const shouldDelete = engaged && dx <= -PLANNER_MEAL_SWIPE_DELETE_THRESHOLD;
|
||||||
|
stopTracking();
|
||||||
|
|
||||||
|
if (shouldDelete) {
|
||||||
|
const slotId = card.getAttribute('data-slot-id');
|
||||||
|
const entryId = card.getAttribute('data-entry-id');
|
||||||
|
row.style.pointerEvents = 'none';
|
||||||
|
row.style.setProperty('--planner-swipe-progress', '1');
|
||||||
|
card.style.willChange = '';
|
||||||
|
card.style.transition = 'transform 160ms cubic-bezier(0.32, 0.72, 0, 1), opacity 160ms ease';
|
||||||
|
card.style.transform = 'translateX(-120%)';
|
||||||
|
card.style.opacity = '0';
|
||||||
|
window.setTimeout(() => {
|
||||||
|
if (removeMealEntry(state, slotId, entryId)) {
|
||||||
|
if (typeof onMealRemoved === 'function') onMealRemoved();
|
||||||
|
else {
|
||||||
|
savePlans(state.plans);
|
||||||
|
renderDayContent(state);
|
||||||
|
}
|
||||||
|
showPlannerToast('Usunięto posiłek z planu dnia');
|
||||||
|
}
|
||||||
|
}, 160);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
engaged = false;
|
||||||
|
dx = 0;
|
||||||
|
resetVisual();
|
||||||
|
};
|
||||||
|
|
||||||
|
const onPointerCancel = (e) => {
|
||||||
|
if (pointerId !== null && e.pointerId !== pointerId) return;
|
||||||
|
stopTracking();
|
||||||
|
engaged = false;
|
||||||
|
dx = 0;
|
||||||
|
resetVisual();
|
||||||
|
};
|
||||||
|
|
||||||
|
card.addEventListener('click', (e) => {
|
||||||
|
if (Date.now() < suppressClickUntil) {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
}
|
||||||
|
}, true);
|
||||||
|
|
||||||
|
card.addEventListener('pointerdown', (e) => {
|
||||||
|
if (pointerId !== null) return;
|
||||||
|
if (e.pointerType === 'mouse' && e.button !== 0) return;
|
||||||
|
|
||||||
|
pointerId = e.pointerId;
|
||||||
|
startX = e.clientX;
|
||||||
|
startY = e.clientY;
|
||||||
|
dx = 0;
|
||||||
|
engaged = false;
|
||||||
|
|
||||||
|
window.addEventListener('pointermove', onPointerMove);
|
||||||
|
window.addEventListener('pointerup', onPointerUp);
|
||||||
|
window.addEventListener('pointercancel', onPointerCancel);
|
||||||
|
});
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function escapeHtml(s) {
|
function escapeHtml(s) {
|
||||||
@@ -481,71 +715,65 @@ function getRecentRecipeIds(plans, limit = 5) {
|
|||||||
return [...seen.keys()];
|
return [...seen.keys()];
|
||||||
}
|
}
|
||||||
|
|
||||||
function recipeCardHtml(r) {
|
function sortPickerRecipes(recipes, plans) {
|
||||||
return `
|
const recentIndex = new Map(
|
||||||
<button type="button" class="planner-pick-recipe w-full flex gap-2.5 p-2.5 rounded-xl border border-[#444442] bg-[#2d2e2b] hover:border-[#6d6c67] hover:bg-[#3a3a37] text-left transition-all" data-recipe-id="${r.id}">
|
getRecentRecipeIds(plans, 12).map((recipeId, index) => [recipeId, index]),
|
||||||
<div class="w-11 h-11 rounded-lg bg-[#3a3a37] overflow-hidden shrink-0">
|
);
|
||||||
${r.image
|
|
||||||
? `<img src="${escapeHtml(r.image)}" alt="" class="w-full h-full object-cover">`
|
return [...recipes].sort((a, b) => {
|
||||||
: `<span class="w-full h-full flex items-center justify-center text-white text-[9px] font-medium">${escapeHtml(r.thumbLabel)}</span>`}
|
const aRecent = recentIndex.has(a.id) ? recentIndex.get(a.id) : Number.POSITIVE_INFINITY;
|
||||||
</div>
|
const bRecent = recentIndex.has(b.id) ? recentIndex.get(b.id) : Number.POSITIVE_INFINITY;
|
||||||
<div class="min-w-0 flex-1 py-0.5">
|
|
||||||
<p class="text-[13px] font-bold text-[#ddd6ca] line-clamp-2">${escapeHtml(r.title)}</p>
|
if (aRecent !== bRecent) return aRecent - bRecent;
|
||||||
<p class="text-[11px] text-[#9b978f] mt-1 tabular-nums">
|
return a.title.localeCompare(b.title, 'pl');
|
||||||
<i class="fas fa-fire text-[#7d7a74] mr-0.5" aria-hidden="true"></i>${r.nutritionPerServing.kcal} kcal
|
});
|
||||||
<span class="mx-1 text-[#6d6c67]">·</span>
|
|
||||||
<i class="fas fa-clock text-[#7d7a74] mr-0.5" aria-hidden="true"></i>${r.minutes} min
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</button>`;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let _pickerSlotRecipes = [];
|
function matchesPickerFilters(recipe, filterState) {
|
||||||
let _pickerPlans = {};
|
const slots = Array.isArray(filterState?.slots) ? filterState.slots : [];
|
||||||
|
const tags = Array.isArray(filterState?.tags) ? filterState.tags : [];
|
||||||
|
const minMinutes = Number.isFinite(filterState?.minMinutes)
|
||||||
|
? filterState.minMinutes
|
||||||
|
: PICKER_FILTER_MIN_MINUTES;
|
||||||
|
const maxMinutes = Number.isFinite(filterState?.maxMinutes)
|
||||||
|
? filterState.maxMinutes
|
||||||
|
: PICKER_FILTER_MAX_MINUTES;
|
||||||
|
|
||||||
function renderPickerList(slotId, plans, query = '') {
|
if (slots.length > 0 && !recipe.allowedSlots.some((slotId) => slots.includes(slotId))) return false;
|
||||||
const slot = MEAL_SLOTS.find((s) => s.id === slotId);
|
|
||||||
const list = document.getElementById('planner-picker-list');
|
|
||||||
const title = document.getElementById('planner-picker-title');
|
|
||||||
const sub = document.getElementById('planner-picker-sub');
|
|
||||||
if (!list || !title || !sub) return;
|
|
||||||
|
|
||||||
title.textContent = 'Wybierz przepis';
|
if (tags.length > 0) {
|
||||||
sub.textContent = slot ? `Dla: ${slot.label}` : '';
|
const recipeTags = (recipe.tags || []).map((tag) => tag.toLowerCase());
|
||||||
|
if (!tags.some((tag) => recipeTags.includes(tag.toLowerCase()))) return false;
|
||||||
const allRecipes = recipesForSlot(slotId);
|
|
||||||
_pickerSlotRecipes = allRecipes;
|
|
||||||
_pickerPlans = plans;
|
|
||||||
|
|
||||||
const q = query.trim().toLowerCase();
|
|
||||||
const filtered = q
|
|
||||||
? allRecipes.filter((r) => r.title.toLowerCase().includes(q) || (r.tags || []).some((t) => t.toLowerCase().includes(q)))
|
|
||||||
: allRecipes;
|
|
||||||
|
|
||||||
if (filtered.length === 0 && q) {
|
|
||||||
list.innerHTML = '<p class="text-sm text-[#9b978f] text-center py-6">Brak wyników.</p>';
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (filtered.length === 0) {
|
|
||||||
list.innerHTML = '<p class="text-sm text-[#9b978f] text-center py-6">Brak dopasowanych przepisów.</p>';
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let html = '';
|
if (minMinutes > PICKER_FILTER_MIN_MINUTES && recipe.minutes < minMinutes) return false;
|
||||||
|
if (maxMinutes < PICKER_FILTER_MAX_MINUTES && recipe.minutes > maxMinutes) return false;
|
||||||
|
|
||||||
if (!q) {
|
return true;
|
||||||
const recentIds = getRecentRecipeIds(plans);
|
}
|
||||||
const recentInSlot = recentIds.map((id) => RECIPES[id]).filter((r) => r && r.allowedSlots.includes(slotId));
|
|
||||||
if (recentInSlot.length > 0) {
|
|
||||||
html += `<p class="text-[10px] font-bold text-gray-400 uppercase tracking-wider px-0.5 pt-1 pb-1"><i class="fas fa-history text-[9px] mr-1"></i>Ostatnio używane</p>`;
|
|
||||||
html += recentInSlot.map(recipeCardHtml).join('');
|
|
||||||
html += `<div class="border-t border-[#444442] my-2"></div>`;
|
|
||||||
html += `<p class="text-[10px] font-bold text-gray-400 uppercase tracking-wider px-0.5 pt-1 pb-1">Wszystkie</p>`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
html += filtered.map(recipeCardHtml).join('');
|
function renderPickerGrid(slotId, plans, query = '') {
|
||||||
list.innerHTML = html;
|
const grid = document.getElementById('planner-picker-grid');
|
||||||
|
const emptyState = document.getElementById('planner-picker-empty-state');
|
||||||
|
if (!grid || !emptyState) return;
|
||||||
|
|
||||||
|
const pickerFilters = window.getPlannerPickerFilterState?.() || {
|
||||||
|
slots: [],
|
||||||
|
tags: [],
|
||||||
|
minMinutes: PICKER_FILTER_MIN_MINUTES,
|
||||||
|
maxMinutes: PICKER_FILTER_MAX_MINUTES,
|
||||||
|
};
|
||||||
|
const filtered = filterRecipesByQuery(recipesForSlot(slotId), query)
|
||||||
|
.filter((recipe) => matchesPickerFilters(recipe, pickerFilters));
|
||||||
|
const sorted = sortPickerRecipes(filtered, plans);
|
||||||
|
|
||||||
|
renderRecipeGrid({
|
||||||
|
gridEl: grid,
|
||||||
|
emptyStateEl: emptyState,
|
||||||
|
recipes: sorted,
|
||||||
|
showSlotLabels: false,
|
||||||
|
cardClassName: 'recipe-list-card',
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function plIngredientWord(n) {
|
function plIngredientWord(n) {
|
||||||
@@ -755,8 +983,15 @@ export function setupMealPlanner() {
|
|||||||
|
|
||||||
const pickerBackdrop = document.getElementById('planner-picker-backdrop');
|
const pickerBackdrop = document.getElementById('planner-picker-backdrop');
|
||||||
const pickerSheet = document.getElementById('planner-picker-sheet');
|
const pickerSheet = document.getElementById('planner-picker-sheet');
|
||||||
|
const pickerScroll = document.getElementById('planner-picker-scroll');
|
||||||
const ingBackdrop = document.getElementById('planner-ing-backdrop');
|
const ingBackdrop = document.getElementById('planner-ing-backdrop');
|
||||||
const ingSheet = document.getElementById('planner-ing-sheet');
|
const ingSheet = document.getElementById('planner-ing-sheet');
|
||||||
|
const pickerFilterState = {
|
||||||
|
slots: [],
|
||||||
|
tags: [],
|
||||||
|
minMinutes: PICKER_FILTER_MIN_MINUTES,
|
||||||
|
maxMinutes: PICKER_FILTER_MAX_MINUTES,
|
||||||
|
};
|
||||||
|
|
||||||
const rerender = () => {
|
const rerender = () => {
|
||||||
syncModeToggle(state.mode);
|
syncModeToggle(state.mode);
|
||||||
@@ -775,7 +1010,7 @@ export function setupMealPlanner() {
|
|||||||
: dayHasAnyMeal(state.plans, day),
|
: dayHasAnyMeal(state.plans, day),
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
renderDayContent(state);
|
renderDayContent(state, persist);
|
||||||
};
|
};
|
||||||
|
|
||||||
const persist = () => {
|
const persist = () => {
|
||||||
@@ -881,8 +1116,13 @@ export function setupMealPlanner() {
|
|||||||
state.pickerSlot = slotId;
|
state.pickerSlot = slotId;
|
||||||
const searchInput = document.getElementById('planner-picker-search');
|
const searchInput = document.getElementById('planner-picker-search');
|
||||||
if (searchInput) searchInput.value = '';
|
if (searchInput) searchInput.value = '';
|
||||||
renderPickerList(slotId, state.plans);
|
const pickerScroll = document.getElementById('planner-picker-scroll');
|
||||||
|
if (pickerScroll) pickerScroll.scrollTop = 0;
|
||||||
|
renderPickerGrid(slotId, state.plans);
|
||||||
openSheet(pickerBackdrop, pickerSheet);
|
openSheet(pickerBackdrop, pickerSheet);
|
||||||
|
window.setTimeout(() => {
|
||||||
|
if (pickerScroll) pickerScroll.scrollTop = 0;
|
||||||
|
}, 40);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const editBtn = e.target.closest('.planner-edit-meal');
|
const editBtn = e.target.closest('.planner-edit-meal');
|
||||||
@@ -903,54 +1143,66 @@ export function setupMealPlanner() {
|
|||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const clearBtn = e.target.closest('.planner-clear-meal');
|
|
||||||
if (clearBtn) {
|
|
||||||
const slotId = clearBtn.getAttribute('data-slot-id');
|
|
||||||
const entryId = clearBtn.getAttribute('data-entry-id');
|
|
||||||
const key = dateKey(state.selected);
|
|
||||||
const arr = state.plans[key]?.[slotId];
|
|
||||||
if (!Array.isArray(arr) || !entryId) return;
|
|
||||||
const next = arr.filter((x) => x && x.id !== entryId);
|
|
||||||
if (!state.plans[key]) state.plans[key] = {};
|
|
||||||
if (next.length === 0) delete state.plans[key][slotId];
|
|
||||||
else state.plans[key][slotId] = next;
|
|
||||||
if (Object.keys(state.plans[key]).length === 0) delete state.plans[key];
|
|
||||||
persist();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const closePicker = () => {
|
const closePicker = () => {
|
||||||
state.pickerSlot = null;
|
state.pickerSlot = null;
|
||||||
|
const searchInput = document.getElementById('planner-picker-search');
|
||||||
|
if (searchInput) searchInput.value = '';
|
||||||
|
if (document.activeElement instanceof HTMLElement) document.activeElement.blur();
|
||||||
closeSheet(pickerBackdrop, pickerSheet);
|
closeSheet(pickerBackdrop, pickerSheet);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
window.getPlannerPickerFilterState = () => ({
|
||||||
|
...pickerFilterState,
|
||||||
|
slots: [...pickerFilterState.slots],
|
||||||
|
tags: [...pickerFilterState.tags],
|
||||||
|
});
|
||||||
|
|
||||||
|
window.applyPlannerPickerFilters = (nextState = {}) => {
|
||||||
|
Object.assign(pickerFilterState, nextState);
|
||||||
|
pickerFilterState.slots = Array.isArray(nextState.slots)
|
||||||
|
? [...nextState.slots]
|
||||||
|
: [...pickerFilterState.slots];
|
||||||
|
pickerFilterState.tags = Array.isArray(nextState.tags)
|
||||||
|
? [...nextState.tags]
|
||||||
|
: [...pickerFilterState.tags];
|
||||||
|
|
||||||
|
if (state.pickerSlot) {
|
||||||
|
const searchValue = document.getElementById('planner-picker-search')?.value || '';
|
||||||
|
renderPickerGrid(state.pickerSlot, state.plans, searchValue);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
document.getElementById('planner-picker-search')?.addEventListener('input', (e) => {
|
document.getElementById('planner-picker-search')?.addEventListener('input', (e) => {
|
||||||
if (state.pickerSlot) {
|
if (state.pickerSlot) {
|
||||||
renderPickerList(state.pickerSlot, state.plans, e.target.value);
|
renderPickerGrid(state.pickerSlot, state.plans, e.target.value);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
bindPlannerSheetDragClose(pickerSheet, closePicker);
|
bindPlannerSheetDragClose(pickerSheet, closePicker, {
|
||||||
|
dragSurface: pickerSheet,
|
||||||
|
scrollEl: pickerScroll,
|
||||||
|
});
|
||||||
bindPlannerSheetDragClose(ingSheet, () => closeSheet(ingBackdrop, ingSheet));
|
bindPlannerSheetDragClose(ingSheet, () => closeSheet(ingBackdrop, ingSheet));
|
||||||
|
|
||||||
pickerBackdrop?.addEventListener('click', closePicker);
|
pickerBackdrop?.addEventListener('click', closePicker);
|
||||||
|
|
||||||
document.getElementById('planner-picker-list')?.addEventListener('click', (e) => {
|
document.getElementById('planner-picker-grid')?.addEventListener('click', (e) => {
|
||||||
const pick = e.target.closest('.planner-pick-recipe');
|
const pick = e.target.closest('.recipe-browser-card');
|
||||||
if (!pick || !state.pickerSlot) return;
|
if (!pick || !state.pickerSlot) return;
|
||||||
const recipeId = pick.getAttribute('data-recipe-id');
|
const recipeId = pick.getAttribute('data-recipe-id');
|
||||||
if (!recipeId || !RECIPES[recipeId]) return;
|
if (!recipeId || !RECIPES[recipeId]) return;
|
||||||
const slotId = state.pickerSlot;
|
const slotId = state.pickerSlot;
|
||||||
closePicker();
|
closePicker();
|
||||||
setTimeout(() => {
|
window.requestAnimationFrame(() => {
|
||||||
window.openMealPlanEditor?.({
|
window.openMealPlanEditor?.({
|
||||||
mode: 'add',
|
mode: 'add',
|
||||||
recipeId,
|
recipeId,
|
||||||
date: state.selected,
|
date: state.selected,
|
||||||
slotId,
|
slotId,
|
||||||
});
|
});
|
||||||
}, 320);
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
document.getElementById('planner-open-ingredients')?.addEventListener('click', () => {
|
document.getElementById('planner-open-ingredients')?.addEventListener('click', () => {
|
||||||
|
|||||||
@@ -1,25 +1,12 @@
|
|||||||
import { RECIPES } from '../data/catalog.js?v=8';
|
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_MIN_MINUTES = 5;
|
||||||
const DEFAULT_MAX_MINUTES = 120;
|
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 = {
|
let filterState = {
|
||||||
query: '',
|
query: '',
|
||||||
@@ -57,107 +44,51 @@ function getFilteredRecipes() {
|
|||||||
return Object.values(RECIPES).filter(matchesFilters);
|
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() {
|
function syncRecipeScrollShadow() {
|
||||||
const scroll = document.getElementById('recipe-scroll');
|
|
||||||
const searchShell = document.getElementById('recipe-search-shell');
|
const searchShell = document.getElementById('recipe-search-shell');
|
||||||
if (!searchShell) return;
|
syncRecipeSearchShellShadow(searchShell);
|
||||||
|
|
||||||
if (!scroll) {
|
|
||||||
searchShell.style.boxShadow = SEARCH_SHELL_BASE_SHADOW;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
searchShell.style.boxShadow = SEARCH_SHELL_BASE_SHADOW;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderAllRecipeCards() {
|
function renderGrid() {
|
||||||
const grid = document.getElementById('recipe-grid');
|
|
||||||
if (!grid) return;
|
|
||||||
|
|
||||||
grid.innerHTML = Object.values(RECIPES).map(renderRecipeCard).join('');
|
|
||||||
}
|
|
||||||
|
|
||||||
function syncVisibleRecipeCards() {
|
|
||||||
const grid = document.getElementById('recipe-grid');
|
const grid = document.getElementById('recipe-grid');
|
||||||
const emptyState = document.getElementById('recipe-empty-state');
|
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 (!grid) return;
|
||||||
|
|
||||||
if (rebuild || !grid.querySelector('[data-recipe-id]')) {
|
renderRecipeGrid({
|
||||||
renderAllRecipeCards();
|
gridEl: grid,
|
||||||
}
|
emptyStateEl: emptyState,
|
||||||
|
recipes: getFilteredRecipes(),
|
||||||
syncVisibleRecipeCards();
|
showSlotLabels: false,
|
||||||
|
cardClassName: 'recipe-list-card',
|
||||||
|
});
|
||||||
|
requestAnimationFrame(syncRecipeScrollShadow);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getRecipeListHTML() {
|
export function getRecipeListHTML() {
|
||||||
return `
|
return `
|
||||||
<div id="main-view" class="flex flex-col h-full absolute inset-0 bg-[#2d2e2b] z-10" style="background:#2d2e2b !important;">
|
<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-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;">
|
<div class="pointer-events-auto">
|
||||||
<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;">
|
${getRecipeSearchFieldHTML({
|
||||||
<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">
|
shellId: 'recipe-search-shell',
|
||||||
<i class="fas fa-sliders-h"></i>
|
inputId: 'recipe-search-input',
|
||||||
</button>
|
placeholder: 'Szukaj przepisów...',
|
||||||
|
filterButtonId: 'recipe-filter-btn',
|
||||||
|
filterButtonAction: 'openFilters()',
|
||||||
|
filterButtonLabel: 'Otwórz filtry',
|
||||||
|
})}
|
||||||
</div>
|
</div>
|
||||||
</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;">
|
${getRecipeGridSectionHTML({
|
||||||
<div id="recipe-grid" class="grid grid-cols-2 gap-3 bg-[#2d2e2b]" style="background:#2d2e2b !important;"></div>
|
scrollId: 'recipe-scroll',
|
||||||
${getEmptyStateHTML()}
|
gridId: 'recipe-grid',
|
||||||
</div>
|
emptyStateId: 'recipe-empty-state',
|
||||||
|
scrollClassName: 'relative flex-1 overflow-y-auto px-4 pt-24 pb-24 bg-[#2d2e2b]',
|
||||||
|
gridClassName: 'grid grid-cols-3 gap-2 bg-[#2d2e2b]',
|
||||||
|
emptyTitle: 'Brak wyników',
|
||||||
|
emptyMessage: 'Zmień kryteria wyszukiwania lub filtry',
|
||||||
|
})}
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
@@ -176,17 +107,24 @@ export function getFilteredCount() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function refreshRecipeList() {
|
export function refreshRecipeList() {
|
||||||
renderGrid({ rebuild: true });
|
renderGrid();
|
||||||
}
|
}
|
||||||
|
|
||||||
export function setupRecipeList() {
|
export function setupRecipeList() {
|
||||||
renderGrid({ rebuild: true });
|
renderGrid();
|
||||||
|
|
||||||
document.getElementById('recipe-search-input')?.addEventListener('input', (e) => {
|
document.getElementById('recipe-search-input')?.addEventListener('input', (e) => {
|
||||||
filterState.query = e.target.value.trim();
|
filterState.query = e.target.value.trim();
|
||||||
renderGrid();
|
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);
|
document.getElementById('recipe-scroll')?.addEventListener('scroll', syncRecipeScrollShadow);
|
||||||
syncRecipeScrollShadow();
|
syncRecipeScrollShadow();
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user