Compare commits
7 Commits
e640b34b0f
...
422b29e3f0
| Author | SHA1 | Date | |
|---|---|---|---|
| 422b29e3f0 | |||
| c341e2e813 | |||
| a29b01e413 | |||
| 8669256ca2 | |||
| 3c271e1632 | |||
| 3258965bcc | |||
| 7cf7aef6e4 |
84
index.html
84
index.html
@@ -7,8 +7,11 @@
|
|||||||
<meta name="apple-mobile-web-app-capable" content="yes">
|
<meta name="apple-mobile-web-app-capable" content="yes">
|
||||||
<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">
|
||||||
|
<meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate">
|
||||||
|
<meta http-equiv="Pragma" content="no-cache">
|
||||||
|
<meta http-equiv="Expires" content="0">
|
||||||
<title>Recipe App - Modular</title>
|
<title>Recipe App - Modular</title>
|
||||||
<link rel="manifest" href="./manifest.webmanifest?v=20260406-42">
|
<link rel="manifest" href="./manifest.webmanifest?v=20260406-63">
|
||||||
<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">
|
||||||
@@ -384,40 +387,30 @@
|
|||||||
}
|
}
|
||||||
#app-bottom-nav .bottom-dock {
|
#app-bottom-nav .bottom-dock {
|
||||||
position: relative;
|
position: relative;
|
||||||
z-index: 0;
|
box-sizing: border-box;
|
||||||
isolation: isolate;
|
width: min(calc(100% - 3.6rem), 20.9rem);
|
||||||
width: min(calc(100% - 2rem), 22.4rem);
|
height: 3.34rem;
|
||||||
min-height: 3.7rem;
|
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||||
align-items: center;
|
align-items: stretch;
|
||||||
gap: 0.1rem;
|
gap: 0.06rem;
|
||||||
padding: 0.35rem 0.45rem;
|
padding: 0.22rem;
|
||||||
border-radius: 999px;
|
border-radius: 1.68rem;
|
||||||
background: #393937;
|
background: #393937;
|
||||||
border: 1px solid #41423f;
|
border: 1px solid #41423f;
|
||||||
box-shadow:
|
box-shadow:
|
||||||
|
inset 0 1px 8px rgba(0, 0, 0, 0.15),
|
||||||
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);
|
|
||||||
backdrop-filter: blur(24px);
|
|
||||||
-webkit-backdrop-filter: blur(24px);
|
|
||||||
pointer-events: auto;
|
pointer-events: auto;
|
||||||
}
|
}
|
||||||
#app-bottom-nav .bottom-dock::after {
|
#app-bottom-nav .nav-slot {
|
||||||
content: '';
|
min-width: 0;
|
||||||
position: absolute;
|
height: 100%;
|
||||||
left: 11%;
|
display: flex;
|
||||||
right: 11%;
|
align-items: center;
|
||||||
bottom: -0.72rem;
|
justify-content: center;
|
||||||
height: 1.05rem;
|
|
||||||
border-radius: 999px;
|
|
||||||
background: rgba(0, 0, 0, 0.36);
|
|
||||||
filter: blur(12px);
|
|
||||||
opacity: 0.9;
|
|
||||||
z-index: -1;
|
|
||||||
pointer-events: none;
|
|
||||||
}
|
}
|
||||||
#app-bottom-nav .nav-tab,
|
#app-bottom-nav .nav-tab,
|
||||||
#app-bottom-nav .nav-action {
|
#app-bottom-nav .nav-action {
|
||||||
@@ -426,15 +419,15 @@
|
|||||||
border: 0;
|
border: 0;
|
||||||
background: transparent !important;
|
background: transparent !important;
|
||||||
box-shadow: none !important;
|
box-shadow: none !important;
|
||||||
width: 2.55rem;
|
width: 2.18rem;
|
||||||
height: 2.55rem;
|
height: 2.18rem;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
border-radius: 999px;
|
border-radius: 1.35rem;
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
justify-self: center;
|
flex: 0 0 auto;
|
||||||
color: #ece8e0;
|
color: #ece8e0;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition:
|
transition:
|
||||||
@@ -455,13 +448,12 @@
|
|||||||
background: rgba(255, 255, 255, 0.04) !important;
|
background: rgba(255, 255, 255, 0.04) !important;
|
||||||
}
|
}
|
||||||
#app-bottom-nav .nav-tab.is-active {
|
#app-bottom-nav .nav-tab.is-active {
|
||||||
width: 2.95rem;
|
width: 100%;
|
||||||
height: 2.95rem;
|
height: 100%;
|
||||||
color: #fff;
|
color: #fff;
|
||||||
background: #2d2e2b !important;
|
background: #2d2e2b !important;
|
||||||
box-shadow:
|
border-radius: 1.46rem;
|
||||||
inset 0 1px 0 rgba(255, 255, 255, 0.04),
|
box-shadow: none !important;
|
||||||
0 10px 20px rgba(0, 0, 0, 0.24) !important;
|
|
||||||
}
|
}
|
||||||
#app-bottom-nav .nav-tab:active,
|
#app-bottom-nav .nav-tab:active,
|
||||||
#app-bottom-nav .nav-action:active {
|
#app-bottom-nav .nav-action:active {
|
||||||
@@ -478,18 +470,22 @@
|
|||||||
padding-inline: 0.7rem;
|
padding-inline: 0.7rem;
|
||||||
}
|
}
|
||||||
#app-bottom-nav .bottom-dock {
|
#app-bottom-nav .bottom-dock {
|
||||||
width: min(calc(100% - 1.4rem), 21.6rem);
|
width: min(calc(100% - 2.4rem), 19.7rem);
|
||||||
min-height: 3.45rem;
|
height: 3.12rem;
|
||||||
padding-inline: 0.35rem;
|
gap: 0.05rem;
|
||||||
|
padding: 0.2rem;
|
||||||
|
border-radius: 1.56rem;
|
||||||
}
|
}
|
||||||
#app-bottom-nav .nav-tab,
|
#app-bottom-nav .nav-tab,
|
||||||
#app-bottom-nav .nav-action {
|
#app-bottom-nav .nav-action {
|
||||||
width: 2.35rem;
|
width: 2.02rem;
|
||||||
height: 2.35rem;
|
height: 2.02rem;
|
||||||
|
border-radius: 1.28rem;
|
||||||
}
|
}
|
||||||
#app-bottom-nav .nav-tab.is-active {
|
#app-bottom-nav .nav-tab.is-active {
|
||||||
width: 2.75rem;
|
height: 100%;
|
||||||
height: 2.75rem;
|
width: 100%;
|
||||||
|
border-radius: 1.36rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -528,7 +524,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
const APP_ASSET_VERSION = '20260406-42';
|
const APP_ASSET_VERSION = '20260406-63';
|
||||||
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';
|
||||||
|
|
||||||
@@ -562,7 +558,7 @@
|
|||||||
})();
|
})();
|
||||||
</script>
|
</script>
|
||||||
<script type="module">
|
<script type="module">
|
||||||
const appVersion = window.__APP_ASSET_VERSION__ || '20260406-42';
|
const appVersion = window.__APP_ASSET_VERSION__ || '20260406-63';
|
||||||
const recoveryKey = `recipe-app-recovery-${appVersion}`;
|
const recoveryKey = `recipe-app-recovery-${appVersion}`;
|
||||||
|
|
||||||
function renderBootstrapError(message) {
|
function renderBootstrapError(message) {
|
||||||
|
|||||||
94
js/app.js
94
js/app.js
@@ -15,6 +15,8 @@ let refreshPantry;
|
|||||||
let setupPantry;
|
let setupPantry;
|
||||||
let getMealPlanEditorHTML;
|
let getMealPlanEditorHTML;
|
||||||
let setupMealPlanEditor;
|
let setupMealPlanEditor;
|
||||||
|
let getBottomNavHTML;
|
||||||
|
let setupBottomNav;
|
||||||
|
|
||||||
const moduleLoadPromise = Promise.all([
|
const moduleLoadPromise = Promise.all([
|
||||||
import(`./views/RecipeList.js?v=${APP_ASSET_VERSION}`),
|
import(`./views/RecipeList.js?v=${APP_ASSET_VERSION}`),
|
||||||
@@ -23,6 +25,7 @@ const moduleLoadPromise = Promise.all([
|
|||||||
import(`./views/MealPlanner.js?v=${APP_ASSET_VERSION}`),
|
import(`./views/MealPlanner.js?v=${APP_ASSET_VERSION}`),
|
||||||
import(`./views/Pantry.js?v=${APP_ASSET_VERSION}`),
|
import(`./views/Pantry.js?v=${APP_ASSET_VERSION}`),
|
||||||
import(`./ui/mealPlanEditor.js?v=${APP_ASSET_VERSION}`),
|
import(`./ui/mealPlanEditor.js?v=${APP_ASSET_VERSION}`),
|
||||||
|
import(`./ui/bottomNav.js?v=${APP_ASSET_VERSION}`),
|
||||||
]).then(([
|
]).then(([
|
||||||
recipeListModule,
|
recipeListModule,
|
||||||
filterModule,
|
filterModule,
|
||||||
@@ -30,6 +33,7 @@ const moduleLoadPromise = Promise.all([
|
|||||||
mealPlannerModule,
|
mealPlannerModule,
|
||||||
pantryModule,
|
pantryModule,
|
||||||
mealPlanEditorModule,
|
mealPlanEditorModule,
|
||||||
|
bottomNavModule,
|
||||||
]) => {
|
]) => {
|
||||||
({ getRecipeListHTML, setupRecipeList } = recipeListModule);
|
({ getRecipeListHTML, setupRecipeList } = recipeListModule);
|
||||||
({ getFilterHTML, setupFilter } = filterModule);
|
({ getFilterHTML, setupFilter } = filterModule);
|
||||||
@@ -37,6 +41,7 @@ const moduleLoadPromise = Promise.all([
|
|||||||
({ getMealPlannerHTML, setupMealPlanner } = mealPlannerModule);
|
({ getMealPlannerHTML, setupMealPlanner } = mealPlannerModule);
|
||||||
({ getPantryHTML, refreshPantry, setupPantry } = pantryModule);
|
({ getPantryHTML, refreshPantry, setupPantry } = pantryModule);
|
||||||
({ getMealPlanEditorHTML, setupMealPlanEditor } = mealPlanEditorModule);
|
({ getMealPlanEditorHTML, setupMealPlanEditor } = mealPlanEditorModule);
|
||||||
|
({ getBottomNavHTML, setupBottomNav } = bottomNavModule);
|
||||||
});
|
});
|
||||||
|
|
||||||
function getAppToastHTML() {
|
function getAppToastHTML() {
|
||||||
@@ -47,92 +52,6 @@ function getAppToastHTML() {
|
|||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getBottomNavHTML() {
|
|
||||||
const isDark = document.documentElement.classList.contains('dark');
|
|
||||||
return `
|
|
||||||
<nav id="app-bottom-nav" aria-label="Główna nawigacja">
|
|
||||||
<div class="bottom-dock">
|
|
||||||
<button type="button" data-tab="recipes" id="nav-recipes" class="nav-tab is-active" aria-label="Przepisy" aria-current="page">
|
|
||||||
<i class="fas fa-book" aria-hidden="true"></i>
|
|
||||||
</button>
|
|
||||||
<button type="button" data-tab="planner" id="nav-planner" class="nav-tab" aria-label="Planer">
|
|
||||||
<i class="far fa-calendar-alt" aria-hidden="true"></i>
|
|
||||||
</button>
|
|
||||||
<button type="button" data-tab="pantry" id="nav-pantry" class="nav-tab" aria-label="Spiżarnia">
|
|
||||||
<i class="fas fa-warehouse" aria-hidden="true"></i>
|
|
||||||
</button>
|
|
||||||
<button type="button" id="nav-theme-toggle" class="nav-action" aria-label="${isDark ? 'Włącz jasny motyw' : 'Włącz ciemny motyw'}" title="${isDark ? 'Jasny motyw' : 'Ciemny motyw'}">
|
|
||||||
<i class="${isDark ? 'fas fa-sun' : 'fas fa-moon'}" aria-hidden="true"></i>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</nav>
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
|
|
||||||
function syncThemeToggleButton(btn, isDark) {
|
|
||||||
if (!btn) return;
|
|
||||||
const icon = btn.querySelector('i');
|
|
||||||
if (icon) icon.className = isDark ? 'fas fa-sun' : 'fas fa-moon';
|
|
||||||
btn.setAttribute('aria-label', isDark ? 'Włącz jasny motyw' : 'Włącz ciemny motyw');
|
|
||||||
btn.title = isDark ? 'Jasny motyw' : 'Ciemny motyw';
|
|
||||||
}
|
|
||||||
|
|
||||||
function setupThemeToggle() {
|
|
||||||
const btn = document.getElementById('nav-theme-toggle');
|
|
||||||
if (!btn) return;
|
|
||||||
|
|
||||||
btn.addEventListener('click', () => {
|
|
||||||
const html = document.documentElement;
|
|
||||||
const isDark = html.classList.toggle('dark');
|
|
||||||
localStorage.setItem('theme', isDark ? 'dark' : 'light');
|
|
||||||
|
|
||||||
syncThemeToggleButton(btn, isDark);
|
|
||||||
|
|
||||||
const meta = document.querySelector('meta[name="theme-color"]');
|
|
||||||
if (meta) meta.setAttribute('content', isDark ? '#161513' : '#f3efe9');
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function setupTabs() {
|
|
||||||
const main = document.getElementById('main-view');
|
|
||||||
const planner = document.getElementById('planner-view');
|
|
||||||
const pantry = document.getElementById('pantry-view');
|
|
||||||
const nav = document.getElementById('app-bottom-nav');
|
|
||||||
if (!main || !planner || !pantry || !nav) return;
|
|
||||||
|
|
||||||
const apply = (tab) => {
|
|
||||||
main.classList.toggle('hidden', tab !== 'recipes');
|
|
||||||
planner.classList.toggle('hidden', tab !== 'planner');
|
|
||||||
pantry.classList.toggle('hidden', tab !== 'pantry');
|
|
||||||
|
|
||||||
if (tab === 'pantry') refreshPantry();
|
|
||||||
|
|
||||||
nav.querySelectorAll('.nav-tab[data-tab]').forEach((btn) => {
|
|
||||||
const id = btn.getAttribute('data-tab');
|
|
||||||
if (btn.hasAttribute('disabled')) return;
|
|
||||||
if (id === 'recipes' || id === 'planner' || id === 'pantry') {
|
|
||||||
const isActive = id === tab;
|
|
||||||
btn.classList.toggle('is-active', isActive);
|
|
||||||
if (isActive) btn.setAttribute('aria-current', 'page');
|
|
||||||
else btn.removeAttribute('aria-current');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
nav.addEventListener('click', (e) => {
|
|
||||||
const btn = e.target.closest('.nav-tab[data-tab]');
|
|
||||||
if (!btn || btn.hasAttribute('disabled')) return;
|
|
||||||
const tab = btn.getAttribute('data-tab');
|
|
||||||
if (tab === 'recipes' || tab === 'planner' || tab === 'pantry') apply(tab);
|
|
||||||
});
|
|
||||||
|
|
||||||
apply('recipes');
|
|
||||||
|
|
||||||
window.refreshStockViews = () => {
|
|
||||||
refreshPantry();
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
let initAppPromise = null;
|
let initAppPromise = null;
|
||||||
|
|
||||||
function renderAppBootError(message) {
|
function renderAppBootError(message) {
|
||||||
@@ -170,8 +89,7 @@ async function initApp() {
|
|||||||
${getAppToastHTML()}
|
${getAppToastHTML()}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
setupTabs();
|
setupBottomNav({ refreshPantry });
|
||||||
setupThemeToggle();
|
|
||||||
setupRecipeList();
|
setupRecipeList();
|
||||||
setupMealPlanner();
|
setupMealPlanner();
|
||||||
setupPantry();
|
setupPantry();
|
||||||
|
|||||||
94
js/ui/bottomNav.js
Normal file
94
js/ui/bottomNav.js
Normal file
@@ -0,0 +1,94 @@
|
|||||||
|
function syncThemeToggleButton(btn, isDark) {
|
||||||
|
if (!btn) return;
|
||||||
|
const icon = btn.querySelector('i');
|
||||||
|
if (icon) icon.className = isDark ? 'fas fa-sun' : 'fas fa-moon';
|
||||||
|
btn.setAttribute('aria-label', isDark ? 'Włącz jasny motyw' : 'Włącz ciemny motyw');
|
||||||
|
btn.title = isDark ? 'Jasny motyw' : 'Ciemny motyw';
|
||||||
|
}
|
||||||
|
|
||||||
|
function setupThemeToggle() {
|
||||||
|
const btn = document.getElementById('nav-theme-toggle');
|
||||||
|
if (!btn) return;
|
||||||
|
|
||||||
|
btn.addEventListener('click', () => {
|
||||||
|
const html = document.documentElement;
|
||||||
|
const isDark = html.classList.toggle('dark');
|
||||||
|
localStorage.setItem('theme', isDark ? 'dark' : 'light');
|
||||||
|
|
||||||
|
syncThemeToggleButton(btn, isDark);
|
||||||
|
|
||||||
|
const meta = document.querySelector('meta[name="theme-color"]');
|
||||||
|
if (meta) meta.setAttribute('content', isDark ? '#161513' : '#f3efe9');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getBottomNavHTML() {
|
||||||
|
const isDark = document.documentElement.classList.contains('dark');
|
||||||
|
return `
|
||||||
|
<nav id="app-bottom-nav" aria-label="Główna nawigacja">
|
||||||
|
<div class="bottom-dock">
|
||||||
|
<div class="nav-slot">
|
||||||
|
<button type="button" data-tab="planner" id="nav-planner" class="nav-tab is-active" aria-label="Planer" aria-current="page">
|
||||||
|
<i class="far fa-calendar-alt" aria-hidden="true"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="nav-slot">
|
||||||
|
<button type="button" data-tab="recipes" id="nav-recipes" class="nav-tab" aria-label="Przepisy">
|
||||||
|
<i class="fas fa-search" aria-hidden="true"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="nav-slot">
|
||||||
|
<button type="button" data-tab="pantry" id="nav-pantry" class="nav-tab" aria-label="Spiżarnia">
|
||||||
|
<i class="fas fa-warehouse" aria-hidden="true"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="nav-slot">
|
||||||
|
<button type="button" id="nav-theme-toggle" class="nav-action" aria-label="${isDark ? 'Włącz jasny motyw' : 'Włącz ciemny motyw'}" title="${isDark ? 'Jasny motyw' : 'Ciemny motyw'}">
|
||||||
|
<i class="${isDark ? 'fas fa-sun' : 'fas fa-moon'}" aria-hidden="true"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function setupBottomNav({ refreshPantry } = {}) {
|
||||||
|
const main = document.getElementById('main-view');
|
||||||
|
const planner = document.getElementById('planner-view');
|
||||||
|
const pantry = document.getElementById('pantry-view');
|
||||||
|
const nav = document.getElementById('app-bottom-nav');
|
||||||
|
if (!main || !planner || !pantry || !nav) return;
|
||||||
|
|
||||||
|
const apply = (tab) => {
|
||||||
|
main.classList.toggle('hidden', tab !== 'recipes');
|
||||||
|
planner.classList.toggle('hidden', tab !== 'planner');
|
||||||
|
pantry.classList.toggle('hidden', tab !== 'pantry');
|
||||||
|
|
||||||
|
if (tab === 'pantry' && typeof refreshPantry === 'function') refreshPantry();
|
||||||
|
|
||||||
|
nav.querySelectorAll('.nav-tab[data-tab]').forEach((btn) => {
|
||||||
|
const id = btn.getAttribute('data-tab');
|
||||||
|
if (btn.hasAttribute('disabled')) return;
|
||||||
|
if (id === 'recipes' || id === 'planner' || id === 'pantry') {
|
||||||
|
const isActive = id === tab;
|
||||||
|
btn.classList.toggle('is-active', isActive);
|
||||||
|
if (isActive) btn.setAttribute('aria-current', 'page');
|
||||||
|
else btn.removeAttribute('aria-current');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
nav.addEventListener('click', (e) => {
|
||||||
|
const btn = e.target.closest('.nav-tab[data-tab]');
|
||||||
|
if (!btn || btn.hasAttribute('disabled')) return;
|
||||||
|
const tab = btn.getAttribute('data-tab');
|
||||||
|
if (tab === 'recipes' || tab === 'planner' || tab === 'pantry') apply(tab);
|
||||||
|
});
|
||||||
|
|
||||||
|
setupThemeToggle();
|
||||||
|
apply('planner');
|
||||||
|
|
||||||
|
window.refreshStockViews = () => {
|
||||||
|
if (typeof refreshPantry === 'function') refreshPantry();
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -46,9 +46,6 @@ export function getMealPlanEditorHTML() {
|
|||||||
<h2 id="mpe-title" class="text-[15px] font-bold text-gray-900 leading-tight"></h2>
|
<h2 id="mpe-title" class="text-[15px] font-bold text-gray-900 leading-tight"></h2>
|
||||||
<p id="mpe-subtitle" class="text-[11px] text-gray-500 mt-0.5 truncate"></p>
|
<p id="mpe-subtitle" class="text-[11px] text-gray-500 mt-0.5 truncate"></p>
|
||||||
</div>
|
</div>
|
||||||
<button id="mpe-close-btn" type="button" class="shrink-0 w-8 h-8 rounded-full border border-gray-200 flex items-center justify-center text-gray-400 hover:text-gray-900 hover:bg-gray-50 transition-colors ml-3">
|
|
||||||
<i class="fas fa-times text-xs"></i>
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div id="mpe-cal-wrap" class="hidden relative z-[1] shrink-0 px-5 pt-3 pb-3 bg-[#2d2e2b]" style="background:#2d2e2b !important; background-image:none !important;">
|
<div id="mpe-cal-wrap" class="hidden relative z-[1] shrink-0 px-5 pt-3 pb-3 bg-[#2d2e2b]" style="background:#2d2e2b !important; background-image:none !important;">
|
||||||
@@ -74,17 +71,16 @@ export function getMealPlanEditorHTML() {
|
|||||||
</div>
|
</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-3 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-16 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="relative z-[1] shrink-0 px-5 pt-2 pb-3 flex justify-center bg-[#2d2e2b]" style="background:#2d2e2b !important; background-image:none !important; 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-3 flex justify-center" style="pointer-events:none; padding-bottom:calc(1.1rem + env(safe-area-inset-bottom));">
|
||||||
<div id="mpe-footer-shadow" class="pointer-events-none absolute inset-x-0 -top-3 h-3 opacity-0 transition-opacity duration-200" style="background:linear-gradient(to top, rgba(0,0,0,0.12), rgba(0,0,0,0.03), rgba(0,0,0,0));"></div>
|
<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 text-white px-4 py-3 rounded-xl font-semibold text-[13px] transition-colors inline-flex items-center justify-center gap-2" style="background:#2d2e2b !important; background-image:none !important; box-shadow:none !important; border-color:#444442 !important;">
|
<span id="mpe-confirm-label">Dodaj do planu</span>
|
||||||
<i class="fas fa-check text-xs"></i> <span id="mpe-confirm-label">Dodaj do planu</span>
|
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -247,8 +243,8 @@ export function setupMealPlanEditor() {
|
|||||||
el.innerHTML = `
|
el.innerHTML = `
|
||||||
<div class="h-full rounded-xl pb-2 flex flex-col" style="background:#2d2e2b !important; background-image:none !important; box-shadow:none !important;">
|
<div class="h-full rounded-xl pb-2 flex flex-col" style="background:#2d2e2b !important; background-image:none !important; box-shadow:none !important;">
|
||||||
<p class="text-[10px] font-bold text-gray-400 uppercase tracking-wider mb-2">Porcje</p>
|
<p class="text-[10px] font-bold text-gray-400 uppercase tracking-wider mb-2">Porcje</p>
|
||||||
<div class="flex items-center pt-2">
|
<div class="flex items-start">
|
||||||
<div class="flex h-[2rem] w-full items-center gap-0.5 rounded-full border px-0.5" style="background:#2f2f2d;border-color:#444442;">
|
<div class="flex h-[2rem] w-full items-center gap-0.5 rounded-full border px-0.5" style="background:#2f2f2d;border-color:#444442;box-shadow:0 2px 8px rgba(0,0,0,0.25);">
|
||||||
<button type="button" id="mpe-serv-minus" class="shrink-0 w-7 h-full flex items-center justify-center rounded-full border-0 bg-transparent text-[#d7d2c8] transition-colors" aria-label="Zmniejsz liczbę porcji">
|
<button type="button" id="mpe-serv-minus" class="shrink-0 w-7 h-full flex items-center justify-center rounded-full border-0 bg-transparent text-[#d7d2c8] transition-colors" aria-label="Zmniejsz liczbę porcji">
|
||||||
<i class="fas fa-minus text-[10px]"></i>
|
<i class="fas fa-minus text-[10px]"></i>
|
||||||
</button>
|
</button>
|
||||||
@@ -287,7 +283,7 @@ export function setupMealPlanEditor() {
|
|||||||
const disp = base * S.servings;
|
const disp = base * S.servings;
|
||||||
const modified = id in S.overrides;
|
const modified = id in S.overrides;
|
||||||
|
|
||||||
const rowStyle = 'background:#393937 !important; background-image:none !important; box-shadow:none !important; border:none !important;';
|
const rowStyle = 'background:#393937 !important; background-image:none !important; box-shadow:0 2px 8px rgba(0,0,0,0.25) !important; border:none !important;';
|
||||||
|
|
||||||
const shuffleBtn = hasAlts
|
const shuffleBtn = hasAlts
|
||||||
? `<button type="button" class="mpe-shuffle shrink-0 w-5 h-5 flex items-center justify-center transition-colors text-gray-400 hover:text-gray-300" style="background:transparent !important; box-shadow:none !important;" data-orig-id="${esc(id)}" aria-label="Wybierz zamiennik składnika"><i class="fas fa-shuffle text-[10px]"></i></button>`
|
? `<button type="button" class="mpe-shuffle shrink-0 w-5 h-5 flex items-center justify-center transition-colors text-gray-400 hover:text-gray-300" style="background:transparent !important; box-shadow:none !important;" data-orig-id="${esc(id)}" aria-label="Wybierz zamiennik składnika"><i class="fas fa-shuffle text-[10px]"></i></button>`
|
||||||
@@ -332,7 +328,7 @@ export function setupMealPlanEditor() {
|
|||||||
const def = INGREDIENTS[a.ingredientId];
|
const def = INGREDIENTS[a.ingredientId];
|
||||||
const name = def?.name || a.ingredientId;
|
const name = def?.name || a.ingredientId;
|
||||||
const disp = a.amount * S.servings;
|
const disp = a.amount * S.servings;
|
||||||
html += `<div class="mpe-ing-row rounded-xl p-2.5" style="background:#393937 !important; background-image:none !important; box-shadow:none !important; border:none !important;" data-ing-id="${esc(a.ingredientId)}" data-type="added">`;
|
html += `<div class="mpe-ing-row rounded-xl p-2.5" style="background:#393937 !important; background-image:none !important; box-shadow:0 2px 8px rgba(0,0,0,0.25) !important; border:none !important;" data-ing-id="${esc(a.ingredientId)}" data-type="added">`;
|
||||||
html += `<div class="flex items-center gap-2">`;
|
html += `<div class="flex items-center gap-2">`;
|
||||||
html += `<div class="flex-1 min-w-0 flex items-center gap-1.5"><span class="text-[12px] font-semibold text-gray-900 truncate">${esc(name)}</span><span class="shrink-0 inline-flex items-center justify-center text-[#8f8b84]" title="Dodany składnik" aria-label="Dodany składnik"><i class="fas fa-plus text-[8px]"></i></span></div>`;
|
html += `<div class="flex-1 min-w-0 flex items-center gap-1.5"><span class="text-[12px] font-semibold text-gray-900 truncate">${esc(name)}</span><span class="shrink-0 inline-flex items-center justify-center text-[#8f8b84]" title="Dodany składnik" aria-label="Dodany składnik"><i class="fas fa-plus text-[8px]"></i></span></div>`;
|
||||||
html += `<button type="button" class="mpe-edit-amt shrink-0 flex items-center gap-1 px-2 py-1 rounded-lg hover:bg-gray-100 transition-colors" data-ing-id="${esc(a.ingredientId)}" data-type="added">`;
|
html += `<button type="button" class="mpe-edit-amt shrink-0 flex items-center gap-1 px-2 py-1 rounded-lg hover:bg-gray-100 transition-colors" data-ing-id="${esc(a.ingredientId)}" data-type="added">`;
|
||||||
@@ -374,7 +370,7 @@ export function setupMealPlanEditor() {
|
|||||||
<input type="text" id="mpe-add-search" class="flex-1 rounded-lg px-3 py-1.5 text-[12px] outline-none placeholder:text-[#8f8b84]" style="background:#2f2f2d !important; border:1px solid #444442; color:#ddd6ca;" placeholder="Szukaj składnika…" value="${esc(S.addQuery)}">
|
<input type="text" id="mpe-add-search" class="flex-1 rounded-lg px-3 py-1.5 text-[12px] outline-none placeholder:text-[#8f8b84]" style="background:#2f2f2d !important; border:1px solid #444442; color:#ddd6ca;" placeholder="Szukaj składnika…" value="${esc(S.addQuery)}">
|
||||||
<button type="button" id="mpe-add-cancel" class="text-[11px] font-semibold px-2 py-1 transition-colors" style="color:#9b978f;">Anuluj</button>
|
<button type="button" id="mpe-add-cancel" class="text-[11px] font-semibold px-2 py-1 transition-colors" style="color:#9b978f;">Anuluj</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="max-h-40 overflow-y-auto space-y-0.5 no-scrollbar" id="mpe-add-results">
|
<div class="max-h-40 overflow-y-auto space-y-1 no-scrollbar" id="mpe-add-results">
|
||||||
${avail.length === 0
|
${avail.length === 0
|
||||||
? '<p class="rounded-lg px-2.5 py-3 text-[11px] text-center" style="background:#2f2f2d !important; color:#9b978f;">Brak wyników</p>'
|
? '<p class="rounded-lg px-2.5 py-3 text-[11px] text-center" style="background:#2f2f2d !important; color:#9b978f;">Brak wyników</p>'
|
||||||
: avail.slice(0, 20).map((i) => `<button type="button" class="mpe-add-pick w-full text-left px-2.5 py-2 rounded-lg transition-colors text-[12px] font-medium" style="background:#2f2f2d !important; color:#ddd6ca;" data-ing-id="${esc(i.id)}">${esc(i.name)} <span class="text-[10px]" style="color:#9b978f;">${esc(i.category)}</span></button>`).join('')}
|
: avail.slice(0, 20).map((i) => `<button type="button" class="mpe-add-pick w-full text-left px-2.5 py-2 rounded-lg transition-colors text-[12px] font-medium" style="background:#2f2f2d !important; color:#ddd6ca;" data-ing-id="${esc(i.id)}">${esc(i.name)} <span class="text-[10px]" style="color:#9b978f;">${esc(i.category)}</span></button>`).join('')}
|
||||||
@@ -413,7 +409,7 @@ export function setupMealPlanEditor() {
|
|||||||
<div class="h-full pb-2 flex flex-col" style="background:#2d2e2b !important; background-image:none !important; box-shadow:none !important;">
|
<div class="h-full pb-2 flex flex-col" style="background:#2d2e2b !important; background-image:none !important; box-shadow:none !important;">
|
||||||
<p class="text-[10px] font-bold text-gray-400 uppercase tracking-wider mb-2">Wartości odżywcze</p>
|
<p class="text-[10px] font-bold text-gray-400 uppercase tracking-wider mb-2">Wartości odżywcze</p>
|
||||||
<div class="flex-1 flex items-center">
|
<div class="flex-1 flex items-center">
|
||||||
<div class="rounded-xl border px-3 py-2" style="border-color:#444442 !important;">
|
<div class="rounded-xl border px-3 py-2" style="background:#2f2f2d !important; border-color:#444442 !important; box-shadow:0 2px 8px rgba(0,0,0,0.25);">
|
||||||
<div class="grid grid-flow-col auto-cols-max gap-3 text-left">
|
<div class="grid grid-flow-col auto-cols-max gap-3 text-left">
|
||||||
<div><span class="block text-[15px] font-semibold text-[#ddd6ca] tabular-nums leading-none">${n.kcal}</span><span class="text-[9px] text-gray-500">kcal</span></div>
|
<div><span class="block text-[15px] font-semibold text-[#ddd6ca] tabular-nums leading-none">${n.kcal}</span><span class="text-[9px] text-gray-500">kcal</span></div>
|
||||||
<div><span class="block text-[15px] font-semibold text-[#ddd6ca] tabular-nums leading-none">${n.protein}<span class="ml-0.5 text-[12px] font-medium text-[#9b978f]">g</span></span><span class="text-[9px] text-gray-500">Białko</span></div>
|
<div><span class="block text-[15px] font-semibold text-[#ddd6ca] tabular-nums leading-none">${n.protein}<span class="ml-0.5 text-[12px] font-medium text-[#9b978f]">g</span></span><span class="text-[9px] text-gray-500">Białko</span></div>
|
||||||
@@ -542,14 +538,39 @@ export function setupMealPlanEditor() {
|
|||||||
|
|
||||||
savePlans(plans);
|
savePlans(plans);
|
||||||
closeEditor();
|
closeEditor();
|
||||||
showAppToast(S.mode === 'edit' ? 'Zapisano zmiany!' : 'Dodano do planera!');
|
|
||||||
window.refreshPlanner?.();
|
window.refreshPlanner?.();
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ── Event bindings ───────────────────────────── */
|
/* ── Event bindings ───────────────────────────── */
|
||||||
|
|
||||||
document.getElementById('mpe-close-btn')?.addEventListener('click', closeEditor);
|
|
||||||
overlay?.addEventListener('click', (e) => { if (e.target === overlay) closeEditor(); });
|
overlay?.addEventListener('click', (e) => { if (e.target === overlay) closeEditor(); });
|
||||||
|
|
||||||
|
/* ── Swipe-to-dismiss ────────────────────────── */
|
||||||
|
const header = sheet?.children[0];
|
||||||
|
if (header && sheet) {
|
||||||
|
let startY = 0, currentY = 0, dragging = false;
|
||||||
|
header.addEventListener('touchstart', (e) => {
|
||||||
|
startY = e.touches[0].clientY;
|
||||||
|
currentY = 0;
|
||||||
|
dragging = true;
|
||||||
|
sheet.style.transition = 'none';
|
||||||
|
}, { passive: true });
|
||||||
|
header.addEventListener('touchmove', (e) => {
|
||||||
|
if (!dragging) return;
|
||||||
|
currentY = Math.max(0, e.touches[0].clientY - startY);
|
||||||
|
sheet.style.transform = `translateY(${currentY}px)`;
|
||||||
|
}, { passive: true });
|
||||||
|
header.addEventListener('touchend', () => {
|
||||||
|
if (!dragging) return;
|
||||||
|
dragging = false;
|
||||||
|
sheet.style.transition = 'transform 300ms cubic-bezier(0.32,0.72,0,1)';
|
||||||
|
if (currentY > 120) {
|
||||||
|
closeEditor();
|
||||||
|
} else {
|
||||||
|
sheet.style.transform = 'translateY(0)';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
document.getElementById('mpe-confirm-btn')?.addEventListener('click', handleConfirm);
|
document.getElementById('mpe-confirm-btn')?.addEventListener('click', handleConfirm);
|
||||||
bindCalendarDayClicks(document.getElementById('mpe-cal-grid'), (date) => {
|
bindCalendarDayClicks(document.getElementById('mpe-cal-grid'), (date) => {
|
||||||
S.date = date;
|
S.date = date;
|
||||||
|
|||||||
@@ -491,7 +491,6 @@ export function setupFilter() {
|
|||||||
|
|
||||||
renderSlotChips();
|
renderSlotChips();
|
||||||
renderTagChips();
|
renderTagChips();
|
||||||
syncLiveFilters();
|
|
||||||
|
|
||||||
showFilterPanel();
|
showFilterPanel();
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -60,7 +60,7 @@ function syncTodayButton(mode, weekStart, monthAnchor, selected) {
|
|||||||
export function getMealPlannerHTML() {
|
export function getMealPlannerHTML() {
|
||||||
return `
|
return `
|
||||||
<div id="planner-view" class="hidden flex flex-col h-full absolute inset-0 overflow-hidden bg-[#2d2e2b] z-10 pb-24">
|
<div id="planner-view" class="hidden flex flex-col h-full absolute inset-0 overflow-hidden bg-[#2d2e2b] z-10 pb-24">
|
||||||
<div class="shrink-0 bg-[#2d2e2b] border-b border-[#444442] mt-3">
|
<div id="planner-cal-bar" class="shrink-0 bg-[#2d2e2b] border-b border-[#444442] mt-3 relative z-10">
|
||||||
${createCalendarTopbarHTML({
|
${createCalendarTopbarHTML({
|
||||||
titleId: 'cal-period-label',
|
titleId: 'cal-period-label',
|
||||||
prevId: 'cal-prev',
|
prevId: 'cal-prev',
|
||||||
@@ -88,7 +88,7 @@ export function getMealPlannerHTML() {
|
|||||||
<div class="h-full flex flex-col" style="background:#2d2e2b !important; background-image:none !important; box-shadow:none !important;">
|
<div class="h-full flex flex-col" style="background:#2d2e2b !important; background-image:none !important; box-shadow:none !important;">
|
||||||
<p class="text-[10px] font-bold text-gray-400 uppercase tracking-wider mb-2">Wartości odżywcze</p>
|
<p class="text-[10px] font-bold text-gray-400 uppercase tracking-wider mb-2">Wartości odżywcze</p>
|
||||||
<div class="flex-1 flex items-center">
|
<div class="flex-1 flex items-center">
|
||||||
<div class="w-full rounded-xl border px-3 py-2.5" style="border-color:#444442 !important;">
|
<div class="w-full rounded-xl border px-3 py-2.5" style="background:#2f2f2d !important; border-color:#444442 !important; box-shadow:0 2px 8px rgba(0,0,0,0.25);">
|
||||||
<div class="grid grid-cols-4 gap-3 text-left">
|
<div class="grid grid-cols-4 gap-3 text-left">
|
||||||
<div class="min-w-0">
|
<div class="min-w-0">
|
||||||
<span id="planner-nutrition-kcal" class="block text-[15px] font-semibold text-[#ddd6ca] tabular-nums leading-none">—</span>
|
<span id="planner-nutrition-kcal" class="block text-[15px] font-semibold text-[#ddd6ca] tabular-nums leading-none">—</span>
|
||||||
@@ -801,6 +801,18 @@ export function setupMealPlanner() {
|
|||||||
rerender();
|
rerender();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/* ── calendar scroll shadow ─────────────────── */
|
||||||
|
const plannerScroll = document.getElementById('planner-scroll');
|
||||||
|
const calBar = document.getElementById('planner-cal-bar');
|
||||||
|
if (plannerScroll && calBar) {
|
||||||
|
const shadow = document.createElement('div');
|
||||||
|
shadow.style.cssText = 'position:absolute;left:0;right:0;bottom:-8px;height:8px;background:linear-gradient(to bottom,rgba(0,0,0,0.25),transparent);opacity:0;transition:opacity 0.2s;pointer-events:none;';
|
||||||
|
calBar.appendChild(shadow);
|
||||||
|
plannerScroll.addEventListener('scroll', () => {
|
||||||
|
shadow.style.opacity = plannerScroll.scrollTop > 2 ? '1' : '0';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
bindCalendarDayClicks(weekGrid, (date) => {
|
bindCalendarDayClicks(weekGrid, (date) => {
|
||||||
state.selected = date;
|
state.selected = date;
|
||||||
rerender();
|
rerender();
|
||||||
|
|||||||
@@ -43,6 +43,21 @@ function setTabButtonState(btn, active) {
|
|||||||
|
|
||||||
export function getRecipeDetailHTML() {
|
export function getRecipeDetailHTML() {
|
||||||
return `
|
return `
|
||||||
|
<style>
|
||||||
|
#rd-tab-bar::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
left: 0; right: 0; bottom: -8px;
|
||||||
|
height: 8px;
|
||||||
|
background: linear-gradient(to bottom, rgba(0,0,0,0.25), transparent);
|
||||||
|
opacity: 0;
|
||||||
|
transition: opacity 0.2s;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
#rd-tab-bar.rd-scrolled::after {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
<div id="recipe-detail-view" class="absolute inset-0 bg-[#2d2e2b] z-30 transition-all duration-300 ease-in-out translate-x-full opacity-0 pointer-events-none flex flex-col overflow-hidden" style="background:#2d2e2b !important; background-image:none !important;">
|
<div id="recipe-detail-view" class="absolute inset-0 bg-[#2d2e2b] z-30 transition-all duration-300 ease-in-out translate-x-full opacity-0 pointer-events-none flex flex-col overflow-hidden" style="background:#2d2e2b !important; background-image:none !important;">
|
||||||
<div class="absolute top-0 w-full p-3.5 flex justify-between z-40 mt-3">
|
<div class="absolute top-0 w-full p-3.5 flex justify-between z-40 mt-3">
|
||||||
<button onclick="closeRecipeDetail()" class="w-9 h-9 rounded-full border flex items-center justify-center transition-opacity opacity-95 hover:opacity-100" style="background:rgba(47,47,45,0.92) !important; backdrop-filter:none !important; border-color:#444442 !important; color:#ddd6ca !important;">
|
<button onclick="closeRecipeDetail()" class="w-9 h-9 rounded-full border flex items-center justify-center transition-opacity opacity-95 hover:opacity-100" style="background:rgba(47,47,45,0.92) !important; backdrop-filter:none !important; border-color:#444442 !important; color:#ddd6ca !important;">
|
||||||
@@ -59,7 +74,7 @@ export function getRecipeDetailHTML() {
|
|||||||
<span id="rd-hero-label" class="absolute inset-0 z-10 flex items-center justify-center font-medium text-[15px]" style="color:#ddd6ca;"></span>
|
<span id="rd-hero-label" class="absolute inset-0 z-10 flex items-center justify-center font-medium text-[15px]" style="color:#ddd6ca;"></span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="bg-[#2d2e2b] rounded-t-3xl -mt-6 relative z-30 pt-6 flex flex-col flex-1 overflow-hidden" style="background:#2d2e2b !important; background-image:none !important;">
|
<div class="bg-[#2d2e2b] rounded-t-3xl -mt-6 relative z-30 pt-6 flex flex-col flex-1 overflow-hidden" style="background:#2d2e2b !important; background-image:none !important; box-shadow:0 -8px 20px rgba(0,0,0,0.35) !important;">
|
||||||
<div class="mb-3 px-5 shrink-0">
|
<div class="mb-3 px-5 shrink-0">
|
||||||
<div class="flex justify-between items-start mb-2.5">
|
<div class="flex justify-between items-start mb-2.5">
|
||||||
<h1 id="rd-title" class="text-xl font-bold leading-tight" style="color:#ddd6ca;"></h1>
|
<h1 id="rd-title" class="text-xl font-bold leading-tight" style="color:#ddd6ca;"></h1>
|
||||||
@@ -70,14 +85,10 @@ export function getRecipeDetailHTML() {
|
|||||||
<i class="fas fa-clock text-[10px]" style="color:#9b978f;"></i>
|
<i class="fas fa-clock text-[10px]" style="color:#9b978f;"></i>
|
||||||
<span id="rd-time"></span>
|
<span id="rd-time"></span>
|
||||||
</div>
|
</div>
|
||||||
<div class="inline-flex items-center gap-1.5 px-2.5 py-1 rounded-full border" style="background:#2f2f2d; border-color:#444442; color:#d7d2c8;">
|
|
||||||
<i class="fas fa-fire text-[10px]" style="color:#9b978f;"></i>
|
|
||||||
<span id="rd-kcal" class="tabular-nums"></span>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex border-b mb-2 px-5 shrink-0" style="border-color:#444442;">
|
<div id="rd-tab-bar" class="flex border-b mb-2 px-5 shrink-0 relative z-10" style="border-color:#444442;">
|
||||||
<button class="flex-1 pb-2.5 text-[13px] border-b-2 rd-tab-btn" data-rd-tab="ingredients" style="color:#ddd6ca; border-bottom-color:#787876; font-weight:600;">Składniki</button>
|
<button class="flex-1 pb-2.5 text-[13px] border-b-2 rd-tab-btn" data-rd-tab="ingredients" style="color:#ddd6ca; border-bottom-color:#787876; font-weight:600;">Składniki</button>
|
||||||
<button class="flex-1 pb-2.5 text-[13px] border-b-2 rd-tab-btn" data-rd-tab="steps" style="color:#9b978f; border-bottom-color:transparent; font-weight:500;">Kroki</button>
|
<button class="flex-1 pb-2.5 text-[13px] border-b-2 rd-tab-btn" data-rd-tab="steps" style="color:#9b978f; border-bottom-color:transparent; font-weight:500;">Kroki</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -127,7 +138,6 @@ function populateDetail(recipeId) {
|
|||||||
}
|
}
|
||||||
document.getElementById('rd-title').textContent = recipe.title;
|
document.getElementById('rd-title').textContent = recipe.title;
|
||||||
document.getElementById('rd-time').textContent = `${recipe.minutes} min`;
|
document.getElementById('rd-time').textContent = `${recipe.minutes} min`;
|
||||||
updateKcalDisplay();
|
|
||||||
|
|
||||||
const tagsHtml = [];
|
const tagsHtml = [];
|
||||||
for (const slotId of recipe.allowedSlots) {
|
for (const slotId of recipe.allowedSlots) {
|
||||||
@@ -155,15 +165,6 @@ function populateDetail(recipeId) {
|
|||||||
|
|
||||||
/* ── helpers ───────────────────────────────────────────── */
|
/* ── helpers ───────────────────────────────────────────── */
|
||||||
|
|
||||||
function updateKcalDisplay() {
|
|
||||||
const el = document.getElementById('rd-kcal');
|
|
||||||
if (!el) return;
|
|
||||||
const recipe = RECIPES[currentRecipeId];
|
|
||||||
if (!recipe) return;
|
|
||||||
const kcal = Math.round(recipe.nutritionPerServing.kcal * currentServings);
|
|
||||||
el.textContent = `${kcal} kcal`;
|
|
||||||
}
|
|
||||||
|
|
||||||
function nutritionForAmount(ingredientId, amount, unit) {
|
function nutritionForAmount(ingredientId, amount, unit) {
|
||||||
const def = INGREDIENTS[ingredientId];
|
const def = INGREDIENTS[ingredientId];
|
||||||
if (!def?.nutritionPer100g) return null;
|
if (!def?.nutritionPer100g) return null;
|
||||||
@@ -216,7 +217,7 @@ function renderNutritionSummary(recipe) {
|
|||||||
<div class="h-full pb-2 flex flex-col" style="background:#2d2e2b !important; background-image:none !important; box-shadow:none !important;">
|
<div class="h-full pb-2 flex flex-col" style="background:#2d2e2b !important; background-image:none !important; box-shadow:none !important;">
|
||||||
<p class="text-[10px] font-bold text-gray-400 uppercase tracking-wider mb-2">Wartości odżywcze</p>
|
<p class="text-[10px] font-bold text-gray-400 uppercase tracking-wider mb-2">Wartości odżywcze</p>
|
||||||
<div class="flex-1 flex items-center">
|
<div class="flex-1 flex items-center">
|
||||||
<div class="rounded-xl border px-3 py-2" style="border-color:#444442 !important;">
|
<div class="rounded-xl border px-3 py-2" style="background:#2f2f2d !important; border-color:#444442 !important; box-shadow:0 2px 8px rgba(0,0,0,0.25);">
|
||||||
<div class="grid grid-flow-col auto-cols-max gap-3 text-left">
|
<div class="grid grid-flow-col auto-cols-max gap-3 text-left">
|
||||||
<div><span class="block text-[15px] font-semibold text-[#ddd6ca] tabular-nums leading-none">${total.kcal}</span><span class="text-[9px] text-gray-500">kcal</span></div>
|
<div><span class="block text-[15px] font-semibold text-[#ddd6ca] tabular-nums leading-none">${total.kcal}</span><span class="text-[9px] text-gray-500">kcal</span></div>
|
||||||
<div><span class="block text-[15px] font-semibold text-[#ddd6ca] tabular-nums leading-none">${total.protein}<span class="ml-0.5 text-[12px] font-medium text-[#9b978f]">g</span></span><span class="text-[9px] text-gray-500">Białko</span></div>
|
<div><span class="block text-[15px] font-semibold text-[#ddd6ca] tabular-nums leading-none">${total.protein}<span class="ml-0.5 text-[12px] font-medium text-[#9b978f]">g</span></span><span class="text-[9px] text-gray-500">Białko</span></div>
|
||||||
@@ -230,8 +231,8 @@ function renderNutritionSummary(recipe) {
|
|||||||
<div class="shrink-0 w-[5.25rem]">
|
<div class="shrink-0 w-[5.25rem]">
|
||||||
<div class="h-full rounded-xl pb-2 flex flex-col" style="background:#2d2e2b !important; background-image:none !important; box-shadow:none !important;">
|
<div class="h-full rounded-xl pb-2 flex flex-col" style="background:#2d2e2b !important; background-image:none !important; box-shadow:none !important;">
|
||||||
<p class="text-[10px] font-bold text-gray-400 uppercase tracking-wider mb-2">Porcje</p>
|
<p class="text-[10px] font-bold text-gray-400 uppercase tracking-wider mb-2">Porcje</p>
|
||||||
<div class="flex items-center pt-2">
|
<div class="flex items-start">
|
||||||
<div class="flex h-[2rem] w-full items-center gap-0.5 rounded-full border px-0.5" style="background:#2f2f2d;border-color:#444442;">
|
<div class="flex h-[2rem] w-full items-center gap-0.5 rounded-full border px-0.5" style="background:#2f2f2d;border-color:#444442;box-shadow:0 2px 8px rgba(0,0,0,0.25);">
|
||||||
<button type="button" id="rd-serv-minus" class="shrink-0 w-7 h-full flex items-center justify-center rounded-full border-0 bg-transparent text-[#d7d2c8] transition-colors" aria-label="Zmniejsz liczbę porcji">
|
<button type="button" id="rd-serv-minus" class="shrink-0 w-7 h-full flex items-center justify-center rounded-full border-0 bg-transparent text-[#d7d2c8] transition-colors" aria-label="Zmniejsz liczbę porcji">
|
||||||
<i class="fas fa-minus text-[10px]"></i>
|
<i class="fas fa-minus text-[10px]"></i>
|
||||||
</button>
|
</button>
|
||||||
@@ -259,7 +260,7 @@ function renderIngredients(recipe) {
|
|||||||
const effectiveName = effectiveDef?.name || effectiveId;
|
const effectiveName = effectiveDef?.name || effectiveId;
|
||||||
const scaledAmount = ing.amount * currentServings;
|
const scaledAmount = ing.amount * currentServings;
|
||||||
const isExpanded = expandedAlternatives.has(origId);
|
const isExpanded = expandedAlternatives.has(origId);
|
||||||
const rowStyle = 'background:#393937 !important; background-image:none !important; box-shadow:none !important; border:none !important;';
|
const rowStyle = 'background:#393937 !important; background-image:none !important; box-shadow:0 2px 8px rgba(0,0,0,0.25) !important; border:none !important;';
|
||||||
|
|
||||||
const toggleBtn = hasAlts
|
const toggleBtn = hasAlts
|
||||||
? `<button type="button" class="rd-alt-toggle shrink-0 w-5 h-5 flex items-center justify-center transition-colors text-gray-400 hover:text-gray-300" style="background:transparent !important; box-shadow:none !important;" data-original-id="${escapeHtml(origId)}" aria-label="Wybierz zamiennik składnika"><i class="fas fa-shuffle text-[10px]"></i></button>`
|
? `<button type="button" class="rd-alt-toggle shrink-0 w-5 h-5 flex items-center justify-center transition-colors text-gray-400 hover:text-gray-300" style="background:transparent !important; box-shadow:none !important;" data-original-id="${escapeHtml(origId)}" aria-label="Wybierz zamiennik składnika"><i class="fas fa-shuffle text-[10px]"></i></button>`
|
||||||
@@ -315,14 +316,12 @@ function renderIngredients(recipe) {
|
|||||||
if (currentServings <= 1) return;
|
if (currentServings <= 1) return;
|
||||||
currentServings--;
|
currentServings--;
|
||||||
renderIngredients(recipe);
|
renderIngredients(recipe);
|
||||||
updateKcalDisplay();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
container.querySelector('#rd-serv-plus')?.addEventListener('click', () => {
|
container.querySelector('#rd-serv-plus')?.addEventListener('click', () => {
|
||||||
if (currentServings >= 12) return;
|
if (currentServings >= 12) return;
|
||||||
currentServings++;
|
currentServings++;
|
||||||
renderIngredients(recipe);
|
renderIngredients(recipe);
|
||||||
updateKcalDisplay();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
container.querySelectorAll('.rd-alt-toggle').forEach((btn) => {
|
container.querySelectorAll('.rd-alt-toggle').forEach((btn) => {
|
||||||
@@ -369,8 +368,8 @@ function renderSteps(recipe) {
|
|||||||
container.innerHTML = `
|
container.innerHTML = `
|
||||||
<div class="space-y-2 pb-5">
|
<div class="space-y-2 pb-5">
|
||||||
${steps.map((step, i) => `
|
${steps.map((step, i) => `
|
||||||
<div class="rounded-xl p-3 flex gap-3" style="${forceBg(RD_THEME.surface)} border:none !important;">
|
<div class="rounded-xl p-3 flex gap-3" style="background:transparent !important; background-image:none !important; box-shadow:none !important; border:none !important;">
|
||||||
<div class="w-6 h-6 rounded-full flex items-center justify-center text-[11px] font-bold shrink-0" style="${forceBgBorder(RD_THEME.surfaceActive, RD_THEME.borderSoft)} color:${RD_THEME.textSecondary} !important;">${i + 1}</div>
|
<div class="w-6 h-6 rounded-full flex items-center justify-center text-[11px] font-bold shrink-0" style="background:transparent !important; border:none !important; box-shadow:none !important; color:${RD_THEME.textSecondary} !important;">${i + 1}</div>
|
||||||
<div class="pt-0.5"><p class="text-[13px] leading-relaxed" style="color:${RD_THEME.textSecondary};">${escapeHtml(step)}</p></div>
|
<div class="pt-0.5"><p class="text-[13px] leading-relaxed" style="color:${RD_THEME.textSecondary};">${escapeHtml(step)}</p></div>
|
||||||
</div>`).join('')}
|
</div>`).join('')}
|
||||||
</div>`;
|
</div>`;
|
||||||
@@ -400,6 +399,16 @@ export function setupRecipeDetail() {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/* ── tab-bar scroll shadow ─────────────────── */
|
||||||
|
|
||||||
|
const scrollContainer = document.querySelector('#recipe-detail-view .overflow-y-auto');
|
||||||
|
const tabBar = document.getElementById('rd-tab-bar');
|
||||||
|
if (scrollContainer && tabBar) {
|
||||||
|
scrollContainer.addEventListener('scroll', () => {
|
||||||
|
tabBar.classList.toggle('rd-scrolled', scrollContainer.scrollTop > 2);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/* ── planner — delegate to MealPlanEditor ─────── */
|
/* ── planner — delegate to MealPlanEditor ─────── */
|
||||||
|
|
||||||
document.getElementById('rd-add-to-planner-btn')?.addEventListener('click', () => {
|
document.getElementById('rd-add-to-planner-btn')?.addEventListener('click', () => {
|
||||||
|
|||||||
@@ -60,7 +60,7 @@ function getFilteredRecipes() {
|
|||||||
function renderRecipeCard(recipe) {
|
function renderRecipeCard(recipe) {
|
||||||
const labels = slotLabelsFor(recipe);
|
const labels = slotLabelsFor(recipe);
|
||||||
return `
|
return `
|
||||||
<div onclick="openRecipeDetail('${escapeHtml(recipe.id)}')" class="rounded-xl overflow-hidden flex flex-col bg-[#393937] cursor-pointer transition-shadow" style="background:#393937 !important; border:none !important; box-shadow:none !important;">
|
<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">
|
<div class="h-32 bg-[#d4d4d4] relative overflow-hidden">
|
||||||
${recipe.image
|
${recipe.image
|
||||||
? `<img src="${escapeHtml(recipe.image)}" alt="${escapeHtml(recipe.title)}" class="w-full h-full object-cover">`
|
? `<img src="${escapeHtml(recipe.image)}" alt="${escapeHtml(recipe.title)}" class="w-full h-full object-cover">`
|
||||||
@@ -81,6 +81,17 @@ function renderRecipeCard(recipe) {
|
|||||||
</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 scroll = document.getElementById('recipe-scroll');
|
||||||
const searchShell = document.getElementById('recipe-search-shell');
|
const searchShell = document.getElementById('recipe-search-shell');
|
||||||
@@ -94,26 +105,41 @@ function syncRecipeScrollShadow() {
|
|||||||
searchShell.style.boxShadow = SEARCH_SHELL_BASE_SHADOW;
|
searchShell.style.boxShadow = SEARCH_SHELL_BASE_SHADOW;
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderGrid() {
|
function renderAllRecipeCards() {
|
||||||
const grid = document.getElementById('recipe-grid');
|
const grid = document.getElementById('recipe-grid');
|
||||||
if (!grid) return;
|
if (!grid) return;
|
||||||
|
|
||||||
const recipes = getFilteredRecipes();
|
grid.innerHTML = Object.values(RECIPES).map(renderRecipeCard).join('');
|
||||||
if (recipes.length === 0) {
|
}
|
||||||
grid.innerHTML = `
|
|
||||||
<div class="col-span-2 flex flex-col items-center justify-center py-16 text-center">
|
function syncVisibleRecipeCards() {
|
||||||
<div class="w-16 h-16 rounded-full bg-gray-100 flex items-center justify-center mb-4">
|
const grid = document.getElementById('recipe-grid');
|
||||||
<i class="fas fa-search text-2xl text-gray-300"></i>
|
const emptyState = document.getElementById('recipe-empty-state');
|
||||||
</div>
|
if (!grid || !emptyState) return;
|
||||||
<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>
|
let visibleCount = 0;
|
||||||
</div>`;
|
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);
|
requestAnimationFrame(syncRecipeScrollShadow);
|
||||||
return;
|
}
|
||||||
|
|
||||||
|
function renderGrid({ rebuild = false } = {}) {
|
||||||
|
const grid = document.getElementById('recipe-grid');
|
||||||
|
if (!grid) return;
|
||||||
|
|
||||||
|
if (rebuild || !grid.querySelector('[data-recipe-id]')) {
|
||||||
|
renderAllRecipeCards();
|
||||||
}
|
}
|
||||||
|
|
||||||
grid.innerHTML = recipes.map(renderRecipeCard).join('');
|
syncVisibleRecipeCards();
|
||||||
requestAnimationFrame(syncRecipeScrollShadow);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getRecipeListHTML() {
|
export function getRecipeListHTML() {
|
||||||
@@ -130,6 +156,7 @@ export function getRecipeListHTML() {
|
|||||||
|
|
||||||
<div id="recipe-scroll" class="relative flex-1 overflow-y-auto px-4 pt-20 pb-24 bg-[#2d2e2b]" style="background:#2d2e2b !important;">
|
<div id="recipe-scroll" class="relative flex-1 overflow-y-auto px-4 pt-20 pb-24 bg-[#2d2e2b]" style="background:#2d2e2b !important;">
|
||||||
<div id="recipe-grid" class="grid grid-cols-2 gap-3 bg-[#2d2e2b]" style="background:#2d2e2b !important;"></div>
|
<div id="recipe-grid" class="grid grid-cols-2 gap-3 bg-[#2d2e2b]" style="background:#2d2e2b !important;"></div>
|
||||||
|
${getEmptyStateHTML()}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
@@ -149,11 +176,11 @@ export function getFilteredCount() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function refreshRecipeList() {
|
export function refreshRecipeList() {
|
||||||
renderGrid();
|
renderGrid({ rebuild: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
export function setupRecipeList() {
|
export function setupRecipeList() {
|
||||||
renderGrid();
|
renderGrid({ rebuild: true });
|
||||||
|
|
||||||
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();
|
||||||
|
|||||||
Reference in New Issue
Block a user