Redesign filter view
This commit is contained in:
85
index.html
85
index.html
@@ -8,7 +8,7 @@
|
|||||||
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
|
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
|
||||||
<meta name="apple-mobile-web-app-title" content="Recipe">
|
<meta name="apple-mobile-web-app-title" content="Recipe">
|
||||||
<title>Recipe App - Modular</title>
|
<title>Recipe App - Modular</title>
|
||||||
<link rel="manifest" href="./manifest.webmanifest?v=20260406-9">
|
<link rel="manifest" href="./manifest.webmanifest?v=20260406-27">
|
||||||
<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">
|
||||||
@@ -279,7 +279,7 @@
|
|||||||
}
|
}
|
||||||
#recipe-search-shell {
|
#recipe-search-shell {
|
||||||
min-height: 2.75rem;
|
min-height: 2.75rem;
|
||||||
border-radius: 1.7rem;
|
border-radius: 1.5rem;
|
||||||
background: #3a3b38 !important;
|
background: #3a3b38 !important;
|
||||||
border: 1px solid rgba(79, 81, 76, 0.95) !important;
|
border: 1px solid rgba(79, 81, 76, 0.95) !important;
|
||||||
box-shadow:
|
box-shadow:
|
||||||
@@ -375,10 +375,13 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div id="app-container" class="relative flex h-dvh min-h-0 w-full flex-col overflow-hidden bg-white">
|
<div id="app-container" class="relative flex h-dvh min-h-0 w-full flex-col overflow-hidden bg-white">
|
||||||
|
<div class="flex h-full items-center justify-center" style="background:#2d2e2b !important;">
|
||||||
|
<div class="text-[13px] font-medium" style="color:#b7ada1 !important;">Ładowanie...</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
const APP_ASSET_VERSION = '20260406-9';
|
const APP_ASSET_VERSION = '20260406-29';
|
||||||
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';
|
||||||
|
|
||||||
@@ -412,27 +415,73 @@
|
|||||||
})();
|
})();
|
||||||
</script>
|
</script>
|
||||||
<script type="module">
|
<script type="module">
|
||||||
const appVersion = window.__APP_ASSET_VERSION__ || '20260406-9';
|
const appVersion = window.__APP_ASSET_VERSION__ || '20260406-29';
|
||||||
|
const recoveryKey = `recipe-app-recovery-${appVersion}`;
|
||||||
|
|
||||||
|
function renderBootstrapError(message) {
|
||||||
|
const appContainer = document.getElementById('app-container');
|
||||||
|
if (!appContainer) return;
|
||||||
|
appContainer.innerHTML = `
|
||||||
|
<div class="flex h-full items-center justify-center px-6 text-center" style="background:#2d2e2b !important;">
|
||||||
|
<div class="max-w-[18rem] rounded-[1.5rem] border px-5 py-6" style="background:#2f2f2d !important; border-color:#444442 !important;">
|
||||||
|
<p class="text-sm font-semibold" style="color:#f2efe8 !important;">Aplikacja nie wystartowała</p>
|
||||||
|
<p class="mt-2 text-xs leading-relaxed" style="color:#b7ada1 !important;">${message}</p>
|
||||||
|
<button type="button" onclick="window.location.reload()" class="mt-4 h-10 px-4 rounded-full border text-[12px] font-semibold" style="background:#23221e !important; border-color:#787876 !important; color:#f2efe8 !important;">Odśwież</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function tryRecoveryReload() {
|
||||||
|
if (sessionStorage.getItem(recoveryKey)) return false;
|
||||||
|
sessionStorage.setItem(recoveryKey, '1');
|
||||||
|
|
||||||
|
try {
|
||||||
|
localStorage.removeItem(APP_VERSION_STORAGE_KEY);
|
||||||
|
|
||||||
|
if ('serviceWorker' in navigator) {
|
||||||
|
const registrations = await navigator.serviceWorker.getRegistrations().catch(() => []);
|
||||||
|
await Promise.all(registrations.map((registration) => registration.unregister().catch(() => false)));
|
||||||
|
}
|
||||||
|
|
||||||
|
if ('caches' in window) {
|
||||||
|
const cacheKeys = await caches.keys().catch(() => []);
|
||||||
|
await Promise.all(cacheKeys.map((key) => caches.delete(key).catch(() => false)));
|
||||||
|
}
|
||||||
|
} catch (_) {
|
||||||
|
/* Ignore recovery cleanup errors. */
|
||||||
|
}
|
||||||
|
|
||||||
|
const nextUrl = new URL(window.location.href);
|
||||||
|
nextUrl.searchParams.set(APP_VERSION_QUERY_KEY, appVersion);
|
||||||
|
nextUrl.searchParams.set('recover', Date.now().toString());
|
||||||
|
window.location.replace(nextUrl.toString());
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await window.__APP_BOOTSTRAP__;
|
await window.__APP_BOOTSTRAP__;
|
||||||
} catch (_) {
|
if ('serviceWorker' in navigator) {
|
||||||
/* Ignore bootstrap cache reset errors and continue loading the app. */
|
try {
|
||||||
}
|
const registration = await navigator.serviceWorker.register(`./sw.js?v=${encodeURIComponent(appVersion)}`, {
|
||||||
|
scope: './',
|
||||||
|
updateViaCache: 'none',
|
||||||
|
});
|
||||||
|
registration.update().catch(() => {});
|
||||||
|
} catch (_) {
|
||||||
|
/* Ignore service worker registration failures. */
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if ('serviceWorker' in navigator) {
|
await import(`./js/app.js?v=${encodeURIComponent(appVersion)}`);
|
||||||
try {
|
sessionStorage.removeItem(recoveryKey);
|
||||||
const registration = await navigator.serviceWorker.register(`./sw.js?v=${encodeURIComponent(appVersion)}`, {
|
} catch (error) {
|
||||||
scope: './',
|
console.error('Bootstrap failed', error);
|
||||||
updateViaCache: 'none',
|
const reloading = await tryRecoveryReload();
|
||||||
});
|
if (!reloading) {
|
||||||
registration.update().catch(() => {});
|
renderBootstrapError('Spróbuj odświeżyć aplikację. Jeśli to nie pomoże, otwórz ją ponownie z Safari.');
|
||||||
} catch (_) {
|
|
||||||
/* Ignore service worker registration failures. */
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
await import(`./js/app.js?v=${encodeURIComponent(appVersion)}`);
|
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
16
js/app.js
16
js/app.js
@@ -132,6 +132,21 @@ function setupTabs() {
|
|||||||
|
|
||||||
let initAppPromise = null;
|
let initAppPromise = null;
|
||||||
|
|
||||||
|
function renderAppBootError(message) {
|
||||||
|
const appContainer = document.getElementById('app-container');
|
||||||
|
if (!appContainer) return;
|
||||||
|
|
||||||
|
appContainer.innerHTML = `
|
||||||
|
<div class="flex h-full items-center justify-center px-6 text-center" style="background:#2d2e2b !important;">
|
||||||
|
<div class="max-w-[18rem] rounded-[1.5rem] border px-5 py-6" style="background:#2f2f2d !important; border-color:#444442 !important;">
|
||||||
|
<p class="text-sm font-semibold" style="color:#f2efe8 !important;">Nie udało się uruchomić aplikacji</p>
|
||||||
|
<p class="mt-2 text-xs leading-relaxed" style="color:#b7ada1 !important;">${message}</p>
|
||||||
|
<button type="button" onclick="window.location.reload()" class="mt-4 h-10 px-4 rounded-full border text-[12px] font-semibold" style="background:#23221e !important; border-color:#787876 !important; color:#f2efe8 !important;">Odśwież aplikację</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
async function initApp() {
|
async function initApp() {
|
||||||
if (initAppPromise) return initAppPromise;
|
if (initAppPromise) return initAppPromise;
|
||||||
|
|
||||||
@@ -171,6 +186,7 @@ async function initApp() {
|
|||||||
function bootApp() {
|
function bootApp() {
|
||||||
initApp().catch((error) => {
|
initApp().catch((error) => {
|
||||||
console.error('Failed to initialize app', error);
|
console.error('Failed to initialize app', error);
|
||||||
|
renderAppBootError('Spróbuj odświeżyć aplikację. Jeśli problem wróci, zamknij ją całkowicie i otwórz ponownie.');
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import { RECIPES } from '../data/catalog.js?v=2';
|
import { RECIPES } from '../data/catalog.js?v=2';
|
||||||
import { MEAL_SLOTS } from '../planner/mealSlots.js';
|
import { MEAL_SLOTS } from '../planner/mealSlots.js';
|
||||||
import { applyFilters, getFilterState, getFilteredCount, refreshRecipeList } from './RecipeList.js';
|
import { applyFilters, getFilterState } from './RecipeList.js';
|
||||||
|
|
||||||
const FILTER_SHEET_TRANSITION = 'transform 300ms cubic-bezier(0.32,0.72,0,1)';
|
const FILTER_PANEL_TRANSITION = 'opacity 180ms ease, transform 180ms ease';
|
||||||
const FILTER_SURFACE = '#2d2e2b';
|
const FILTER_SURFACE = '#2d2e2b';
|
||||||
const FILTER_SURFACE_SOFT = '#2f2f2d';
|
const FILTER_SURFACE_SOFT = '#2f2f2d';
|
||||||
const FILTER_BORDER = '#444442';
|
const FILTER_BORDER = '#444442';
|
||||||
@@ -13,12 +13,13 @@ const FILTER_TEXT_SECONDARY = '#d7d2c8';
|
|||||||
const FILTER_TEXT_MUTED = '#9b978f';
|
const FILTER_TEXT_MUTED = '#9b978f';
|
||||||
const FILTER_TEXT_DIM = '#7d7a74';
|
const FILTER_TEXT_DIM = '#7d7a74';
|
||||||
const FILTER_TEXT_ACTIVE = '#f2efe8';
|
const FILTER_TEXT_ACTIVE = '#f2efe8';
|
||||||
const FILTER_TRACK = '#444442';
|
const FILTER_TRACK = '#393937';
|
||||||
const FILTER_TRACK_FILL = '#9b978f';
|
const FILTER_TRACK_FILL = '#56534f';
|
||||||
const PREP_TIME_MIN = 5;
|
const PREP_TIME_MIN = 5;
|
||||||
const PREP_TIME_MAX = 120;
|
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)';
|
||||||
|
|
||||||
function escapeHtml(s) {
|
function escapeHtml(s) {
|
||||||
return String(s)
|
return String(s)
|
||||||
@@ -53,31 +54,23 @@ export function getFilterHTML() {
|
|||||||
width: 1rem;
|
width: 1rem;
|
||||||
height: 1rem;
|
height: 1rem;
|
||||||
border-radius: 9999px;
|
border-radius: 9999px;
|
||||||
border: 2px solid ${FILTER_SURFACE};
|
border: 1px solid rgba(242,239,232,0.16);
|
||||||
background: ${FILTER_TEXT_ACTIVE};
|
background: ${FILTER_TRACK_FILL};
|
||||||
box-shadow: 0 0 0 1px ${FILTER_BORDER_ACTIVE};
|
box-shadow: 0 0 0 1px rgba(0,0,0,0.12);
|
||||||
touch-action: none;
|
touch-action: none;
|
||||||
outline: none;
|
outline: none;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
<div id="filter-view" class="absolute inset-0 z-[55] hidden items-end opacity-0 transition-opacity duration-200" style="pointer-events:none; background:transparent !important; background-image:none !important;" aria-hidden="true">
|
<div id="filter-view" class="absolute inset-0 z-[55] hidden opacity-0 transition-opacity duration-150" style="pointer-events:none; background:transparent !important; background-image:none !important;" aria-hidden="true">
|
||||||
<div id="filter-sheet" class="relative w-full flex flex-col overflow-hidden rounded-t-3xl" style="background:${FILTER_SURFACE} !important; background-image:none !important; max-height:calc(100dvh - 4.5rem); transform:translateY(100%); transition:${FILTER_SHEET_TRANSITION}; box-shadow:0 -26px 46px rgba(0,0,0,0.28), 0 -6px 16px rgba(0,0,0,0.22);">
|
<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-4 pt-3 pb-3 border-b" style="background:${FILTER_SURFACE} !important; background-image:none !important; border-color:${FILTER_BORDER} !important;">
|
<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="w-10 h-1 rounded-full mx-auto mb-3" style="background:rgba(109,108,103,0.75);" aria-hidden="true"></div>
|
<div class="min-w-0 flex items-center justify-end gap-2">
|
||||||
<div class="flex items-center gap-2">
|
|
||||||
<button id="filter-close-btn" type="button" class="shrink-0 w-9 h-9 rounded-full border flex items-center justify-center transition-colors" style="background:${FILTER_SURFACE_SOFT} !important; border-color:${FILTER_BORDER} !important; color:${FILTER_TEXT_SECONDARY} !important;" aria-label="Zamknij filtry">
|
|
||||||
<i class="fas fa-arrow-left text-[12px]"></i>
|
|
||||||
</button>
|
|
||||||
<div class="min-w-0 flex-1 pr-2">
|
|
||||||
<h2 class="text-[15px] font-bold leading-tight" style="color:${FILTER_TEXT_PRIMARY};">Filtry</h2>
|
|
||||||
<p class="text-[11px] mt-1" style="color:${FILTER_TEXT_MUTED};">Dopasuj listę przepisów do swoich potrzeb.</p>
|
|
||||||
</div>
|
|
||||||
<button id="filter-clear-btn" type="button" class="shrink-0 h-8 px-3 rounded-full border text-[11px] font-semibold transition-colors" style="background:${FILTER_SURFACE_SOFT} !important; border-color:${FILTER_BORDER} !important; color:${FILTER_TEXT_SECONDARY} !important;">Wyczyść</button>
|
<button id="filter-clear-btn" type="button" class="shrink-0 h-8 px-3 rounded-full border text-[11px] font-semibold transition-colors" style="background:${FILTER_SURFACE_SOFT} !important; border-color:${FILTER_BORDER} !important; color:${FILTER_TEXT_SECONDARY} !important;">Wyczyść</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="min-h-0 flex-1 overflow-y-auto no-scrollbar px-4 py-4 space-y-4" 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 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>
|
||||||
@@ -129,13 +122,6 @@ export function getFilterHTML() {
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="shrink-0 px-4 pt-2 pb-4" style="background:${FILTER_SURFACE} !important; background-image:none !important; padding-bottom:calc(1rem + env(safe-area-inset-bottom));">
|
|
||||||
<button id="filter-apply-btn" type="button" class="w-full py-3 rounded-xl border font-semibold text-[13px] transition-colors flex items-center justify-center gap-2" style="background:${FILTER_SURFACE} !important; background-image:none !important; box-shadow:none !important; border-color:${FILTER_BORDER} !important; color:${FILTER_TEXT_ACTIVE} !important;">
|
|
||||||
<i class="fas fa-check text-[11px]" style="color:${FILTER_TEXT_MUTED};" aria-hidden="true"></i>
|
|
||||||
<span id="filter-apply-label">Pokaż wyniki</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
@@ -226,37 +212,82 @@ function syncTimeRangeUI() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function showFilterSheet() {
|
function positionFilterPanel() {
|
||||||
const view = document.getElementById('filter-view');
|
const view = document.getElementById('filter-view');
|
||||||
const sheet = document.getElementById('filter-sheet');
|
const panel = document.getElementById('filter-panel');
|
||||||
if (!view || !sheet) return;
|
const body = document.getElementById('filter-panel-body');
|
||||||
|
const searchShell = document.getElementById('recipe-search-shell');
|
||||||
|
const button = document.getElementById('recipe-filter-btn');
|
||||||
|
if (!view || !panel || !button) return;
|
||||||
|
|
||||||
|
const viewRect = view.getBoundingClientRect();
|
||||||
|
const anchorRect = (searchShell || button).getBoundingClientRect();
|
||||||
|
const gap = 8;
|
||||||
|
const margin = 12;
|
||||||
|
const width = Math.min(anchorRect.width, viewRect.width - margin * 2);
|
||||||
|
const top = Math.max(margin, anchorRect.bottom - viewRect.top + gap);
|
||||||
|
const left = Math.max(
|
||||||
|
margin,
|
||||||
|
Math.min(anchorRect.left - viewRect.left, viewRect.width - width - margin),
|
||||||
|
);
|
||||||
|
const maxHeight = Math.max(220, viewRect.height - top - margin);
|
||||||
|
|
||||||
|
panel.style.width = `${width}px`;
|
||||||
|
panel.style.left = `${left}px`;
|
||||||
|
panel.style.top = `${top}px`;
|
||||||
|
panel.style.maxHeight = `${maxHeight}px`;
|
||||||
|
if (body) body.style.maxHeight = `${maxHeight - 56}px`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function setRecipeAreaBlur(visible) {
|
||||||
|
const recipeScroll = document.getElementById('recipe-scroll');
|
||||||
|
if (!recipeScroll) return;
|
||||||
|
|
||||||
|
recipeScroll.style.transition = 'filter 180ms ease, opacity 180ms ease';
|
||||||
|
recipeScroll.style.willChange = 'filter, opacity';
|
||||||
|
recipeScroll.style.filter = visible ? FILTER_RECIPE_BLUR : 'none';
|
||||||
|
recipeScroll.style.opacity = visible ? '0.78' : '1';
|
||||||
|
}
|
||||||
|
|
||||||
|
function showFilterPanel() {
|
||||||
|
const view = document.getElementById('filter-view');
|
||||||
|
const panel = document.getElementById('filter-panel');
|
||||||
|
if (!view || !panel) return;
|
||||||
|
|
||||||
clearTimeout(closeTimer);
|
clearTimeout(closeTimer);
|
||||||
view.classList.remove('hidden');
|
view.classList.remove('hidden');
|
||||||
view.classList.add('flex', 'filter-open');
|
view.classList.add('filter-open');
|
||||||
view.style.pointerEvents = 'auto';
|
view.style.pointerEvents = 'auto';
|
||||||
view.setAttribute('aria-hidden', 'false');
|
view.setAttribute('aria-hidden', 'false');
|
||||||
|
positionFilterPanel();
|
||||||
|
setRecipeAreaBlur(true);
|
||||||
|
|
||||||
requestAnimationFrame(() => {
|
requestAnimationFrame(() => {
|
||||||
view.classList.add('opacity-100');
|
view.classList.add('opacity-100');
|
||||||
sheet.style.transform = 'translateY(0)';
|
panel.style.opacity = '1';
|
||||||
|
panel.style.transform = 'translateY(0) scale(1)';
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function hideFilterSheet() {
|
function hideFilterPanel() {
|
||||||
const view = document.getElementById('filter-view');
|
const view = document.getElementById('filter-view');
|
||||||
const sheet = document.getElementById('filter-sheet');
|
const panel = document.getElementById('filter-panel');
|
||||||
if (!view || !sheet) return;
|
if (!view || !panel) return;
|
||||||
|
|
||||||
view.classList.remove('opacity-100', 'filter-open');
|
view.classList.remove('opacity-100', 'filter-open');
|
||||||
view.style.pointerEvents = 'none';
|
view.style.pointerEvents = 'none';
|
||||||
view.setAttribute('aria-hidden', 'true');
|
view.setAttribute('aria-hidden', 'true');
|
||||||
sheet.style.transform = 'translateY(100%)';
|
panel.style.opacity = '0';
|
||||||
|
panel.style.transform = 'translateY(-0.5rem) scale(0.98)';
|
||||||
|
setRecipeAreaBlur(false);
|
||||||
|
|
||||||
closeTimer = setTimeout(() => {
|
closeTimer = setTimeout(() => {
|
||||||
view.classList.add('hidden');
|
view.classList.add('hidden');
|
||||||
view.classList.remove('flex');
|
}, 180);
|
||||||
}, 300);
|
}
|
||||||
|
|
||||||
|
function isFilterPanelOpen() {
|
||||||
|
return document.getElementById('filter-view')?.classList.contains('filter-open');
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderSlotChips() {
|
function renderSlotChips() {
|
||||||
@@ -275,7 +306,7 @@ function renderSlotChips() {
|
|||||||
if (idx >= 0) localSlots.splice(idx, 1);
|
if (idx >= 0) localSlots.splice(idx, 1);
|
||||||
else localSlots.push(id);
|
else localSlots.push(id);
|
||||||
renderSlotChips();
|
renderSlotChips();
|
||||||
updateResultCount();
|
syncLiveFilters();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -297,21 +328,18 @@ function renderTagChips() {
|
|||||||
if (idx >= 0) localTags.splice(idx, 1);
|
if (idx >= 0) localTags.splice(idx, 1);
|
||||||
else localTags.push(tag);
|
else localTags.push(tag);
|
||||||
renderTagChips();
|
renderTagChips();
|
||||||
updateResultCount();
|
syncLiveFilters();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateResultCount() {
|
function syncLiveFilters() {
|
||||||
applyFilters({
|
applyFilters({
|
||||||
slots: localSlots,
|
slots: localSlots,
|
||||||
tags: localTags,
|
tags: localTags,
|
||||||
minMinutes: localMinMinutes,
|
minMinutes: localMinMinutes,
|
||||||
maxMinutes: localMaxMinutes,
|
maxMinutes: localMaxMinutes,
|
||||||
});
|
});
|
||||||
const count = getFilteredCount();
|
|
||||||
const applyBtnLabel = document.getElementById('filter-apply-label');
|
|
||||||
if (applyBtnLabel) applyBtnLabel.textContent = `Pokaż ${count} wyników`;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function setupFilter() {
|
export function setupFilter() {
|
||||||
@@ -321,6 +349,8 @@ export function setupFilter() {
|
|||||||
const filterView = document.getElementById('filter-view');
|
const filterView = document.getElementById('filter-view');
|
||||||
let activeTimeHandle = null;
|
let activeTimeHandle = null;
|
||||||
|
|
||||||
|
setRecipeAreaBlur(false);
|
||||||
|
|
||||||
function valueFromPointer(clientX) {
|
function valueFromPointer(clientX) {
|
||||||
if (!rangeTrack) return PREP_TIME_MIN;
|
if (!rangeTrack) return PREP_TIME_MIN;
|
||||||
const rect = rangeTrack.getBoundingClientRect();
|
const rect = rangeTrack.getBoundingClientRect();
|
||||||
@@ -339,7 +369,7 @@ export function setupFilter() {
|
|||||||
localMaxMinutes = Math.min(localMaxMinutes, PREP_TIME_MAX);
|
localMaxMinutes = Math.min(localMaxMinutes, PREP_TIME_MAX);
|
||||||
}
|
}
|
||||||
syncTimeRangeUI();
|
syncTimeRangeUI();
|
||||||
updateResultCount();
|
syncLiveFilters();
|
||||||
}
|
}
|
||||||
|
|
||||||
function stopTimeHandleDrag() {
|
function stopTimeHandleDrag() {
|
||||||
@@ -418,22 +448,16 @@ export function setupFilter() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
filterView?.addEventListener('click', (e) => {
|
filterView?.addEventListener('click', (e) => {
|
||||||
if (e.target === filterView) window.closeFilters();
|
if (e.composedPath().includes(document.getElementById('filter-panel'))) return;
|
||||||
});
|
|
||||||
|
|
||||||
document.getElementById('filter-close-btn')?.addEventListener('click', () => {
|
|
||||||
window.closeFilters();
|
window.closeFilters();
|
||||||
});
|
});
|
||||||
|
|
||||||
document.getElementById('filter-apply-btn')?.addEventListener('click', () => {
|
window.addEventListener('resize', () => {
|
||||||
applyFilters({
|
if (isFilterPanelOpen()) positionFilterPanel();
|
||||||
slots: localSlots,
|
});
|
||||||
tags: localTags,
|
|
||||||
minMinutes: localMinMinutes,
|
document.addEventListener('keydown', (e) => {
|
||||||
maxMinutes: localMaxMinutes,
|
if (e.key === 'Escape' && isFilterPanelOpen()) window.closeFilters();
|
||||||
});
|
|
||||||
refreshRecipeList();
|
|
||||||
window.closeFilters();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
document.getElementById('filter-clear-btn')?.addEventListener('click', () => {
|
document.getElementById('filter-clear-btn')?.addEventListener('click', () => {
|
||||||
@@ -444,16 +468,15 @@ export function setupFilter() {
|
|||||||
syncTimeRangeUI();
|
syncTimeRangeUI();
|
||||||
renderSlotChips();
|
renderSlotChips();
|
||||||
renderTagChips();
|
renderTagChips();
|
||||||
applyFilters({
|
syncLiveFilters();
|
||||||
slots: [],
|
|
||||||
tags: [],
|
|
||||||
minMinutes: PREP_TIME_MIN,
|
|
||||||
maxMinutes: PREP_TIME_MAX,
|
|
||||||
});
|
|
||||||
updateResultCount();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
window.openFilters = () => {
|
window.openFilters = () => {
|
||||||
|
if (isFilterPanelOpen()) {
|
||||||
|
window.closeFilters();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const state = getFilterState();
|
const state = getFilterState();
|
||||||
localSlots = [...state.slots];
|
localSlots = [...state.slots];
|
||||||
localTags = [...state.tags];
|
localTags = [...state.tags];
|
||||||
@@ -468,13 +491,13 @@ export function setupFilter() {
|
|||||||
|
|
||||||
renderSlotChips();
|
renderSlotChips();
|
||||||
renderTagChips();
|
renderTagChips();
|
||||||
updateResultCount();
|
syncLiveFilters();
|
||||||
|
|
||||||
showFilterSheet();
|
showFilterPanel();
|
||||||
};
|
};
|
||||||
|
|
||||||
window.closeFilters = () => {
|
window.closeFilters = () => {
|
||||||
stopTimeHandleDrag();
|
stopTimeHandleDrag();
|
||||||
hideFilterSheet();
|
hideFilterPanel();
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,6 +12,9 @@ function escapeHtml(s) {
|
|||||||
const slotLabelMap = Object.fromEntries(MEAL_SLOTS.map((s) => [s.id, s.label]));
|
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 =
|
||||||
|
'inset 0 1px 0 rgba(255,255,255,0.045), 0 0 0 1px rgba(255,255,255,0.015)';
|
||||||
|
const SEARCH_HEADER_SCROLLED_SHADOW = '0 10px 16px rgba(0,0,0,0.34)';
|
||||||
|
|
||||||
function slotLabelsFor(recipe) {
|
function slotLabelsFor(recipe) {
|
||||||
return (recipe.allowedSlots || [])
|
return (recipe.allowedSlots || [])
|
||||||
@@ -79,6 +82,26 @@ function renderRecipeCard(recipe) {
|
|||||||
</div>`;
|
</div>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function syncRecipeScrollShadow() {
|
||||||
|
const scroll = document.getElementById('recipe-scroll');
|
||||||
|
const shadow = document.getElementById('recipe-top-bar-shadow');
|
||||||
|
const header = document.getElementById('recipe-top-bar');
|
||||||
|
const searchShell = document.getElementById('recipe-search-shell');
|
||||||
|
if (!shadow || !header || !searchShell) return;
|
||||||
|
|
||||||
|
if (!scroll) {
|
||||||
|
shadow.style.opacity = '0';
|
||||||
|
header.style.boxShadow = 'none';
|
||||||
|
searchShell.style.boxShadow = SEARCH_SHELL_BASE_SHADOW;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const isScrolled = scroll.scrollTop > 6;
|
||||||
|
shadow.style.opacity = isScrolled ? '1' : '0';
|
||||||
|
header.style.boxShadow = isScrolled ? SEARCH_HEADER_SCROLLED_SHADOW : 'none';
|
||||||
|
searchShell.style.boxShadow = SEARCH_SHELL_BASE_SHADOW;
|
||||||
|
}
|
||||||
|
|
||||||
function renderGrid() {
|
function renderGrid() {
|
||||||
const grid = document.getElementById('recipe-grid');
|
const grid = document.getElementById('recipe-grid');
|
||||||
if (!grid) return;
|
if (!grid) return;
|
||||||
@@ -93,25 +116,28 @@ function renderGrid() {
|
|||||||
<p class="text-sm font-semibold text-gray-700">Brak wyników</p>
|
<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>
|
<p class="text-xs text-gray-500 mt-1 max-w-[220px] leading-relaxed">Zmień kryteria wyszukiwania lub filtry</p>
|
||||||
</div>`;
|
</div>`;
|
||||||
|
requestAnimationFrame(syncRecipeScrollShadow);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
grid.innerHTML = recipes.map(renderRecipeCard).join('');
|
grid.innerHTML = recipes.map(renderRecipeCard).join('');
|
||||||
|
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 class="px-4 pt-3 pb-4 mt-2" style="background:#2d2e2b !important; border:none !important;">
|
<div id="recipe-top-bar" class="relative z-[2] px-4 pt-3 pb-4 mt-2" style="background:#2d2e2b !important; border:none !important; transition:box-shadow 180ms ease;">
|
||||||
<div id="recipe-search-shell" class="relative flex items-center w-full overflow-hidden" style="background:#3a3b38 !important; border:1px solid rgba(79,81,76,0.95) !important; border-radius:1.7rem !important; box-shadow:inset 0 1px 0 rgba(255,255,255,0.045), 0 0 0 1px rgba(255,255,255,0.015) !important;">
|
<div id="recipe-search-shell" class="relative z-[1] flex items-center w-full overflow-hidden" style="background:#3a3b38 !important; border:1px solid rgba(79,81,76,0.95) !important; border-radius:1.5rem !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;">
|
<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">
|
<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>
|
<i class="fas fa-sliders-h"></i>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
<div id="recipe-top-bar-shadow" class="pointer-events-none absolute inset-x-0 bottom-0 z-[0] h-4 translate-y-1 opacity-0 transition-opacity duration-200" style="background:linear-gradient(to bottom, rgba(0,0,0,0.46), rgba(0,0,0,0.18), rgba(0,0,0,0)); filter:blur(7px);"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex-1 overflow-y-auto px-4 pt-4 pb-24 bg-[#2d2e2b]" style="background:#2d2e2b !important;">
|
<div id="recipe-scroll" class="relative flex-1 overflow-y-auto px-4 pt-4 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>
|
<div id="recipe-grid" class="grid grid-cols-2 gap-3 bg-[#2d2e2b]" style="background:#2d2e2b !important;"></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -142,4 +168,7 @@ export function setupRecipeList() {
|
|||||||
filterState.query = e.target.value.trim();
|
filterState.query = e.target.value.trim();
|
||||||
renderGrid();
|
renderGrid();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
document.getElementById('recipe-scroll')?.addEventListener('scroll', syncRecipeScrollShadow);
|
||||||
|
syncRecipeScrollShadow();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
"name": "Recipe App",
|
"name": "Recipe App",
|
||||||
"short_name": "Recipe",
|
"short_name": "Recipe",
|
||||||
"description": "Plan posiłków, spiżarnia i zakupy",
|
"description": "Plan posiłków, spiżarnia i zakupy",
|
||||||
"start_url": "./?appv=20260406-9",
|
"start_url": "./?appv=20260406-29",
|
||||||
"scope": "./",
|
"scope": "./",
|
||||||
"display": "standalone",
|
"display": "standalone",
|
||||||
"background_color": "#f3f4f6",
|
"background_color": "#f3f4f6",
|
||||||
|
|||||||
Reference in New Issue
Block a user