Extract menu to a separate component
This commit is contained in:
@@ -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-58">
|
<link rel="manifest" href="./manifest.webmanifest?v=20260406-60">
|
||||||
<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">
|
||||||
@@ -520,7 +520,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
const APP_ASSET_VERSION = '20260406-58';
|
const APP_ASSET_VERSION = '20260406-60';
|
||||||
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';
|
||||||
|
|
||||||
@@ -554,7 +554,7 @@
|
|||||||
})();
|
})();
|
||||||
</script>
|
</script>
|
||||||
<script type="module">
|
<script type="module">
|
||||||
const appVersion = window.__APP_ASSET_VERSION__ || '20260406-58';
|
const appVersion = window.__APP_ASSET_VERSION__ || '20260406-60';
|
||||||
const recoveryKey = `recipe-app-recovery-${appVersion}`;
|
const recoveryKey = `recipe-app-recovery-${appVersion}`;
|
||||||
|
|
||||||
function renderBootstrapError(message) {
|
function renderBootstrapError(message) {
|
||||||
|
|||||||
102
js/app.js
102
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,100 +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">
|
|
||||||
<div class="nav-slot">
|
|
||||||
<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>
|
|
||||||
</div>
|
|
||||||
<div class="nav-slot">
|
|
||||||
<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>
|
|
||||||
</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>
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
|
|
||||||
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) {
|
||||||
@@ -178,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="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>
|
||||||
|
</div>
|
||||||
|
<div class="nav-slot">
|
||||||
|
<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>
|
||||||
|
</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('recipes');
|
||||||
|
|
||||||
|
window.refreshStockViews = () => {
|
||||||
|
if (typeof refreshPantry === 'function') refreshPantry();
|
||||||
|
};
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user