Add dark mode
All checks were successful
Build and Deploy / build-and-push (push) Successful in 1m12s
All checks were successful
Build and Deploy / build-and-push (push) Successful in 1m12s
This commit is contained in:
141
index.html
141
index.html
@@ -3,7 +3,7 @@
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta name="theme-color" content="#111827">
|
||||
<meta name="theme-color" content="#0d0d0d">
|
||||
<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-title" content="Recipe">
|
||||
@@ -22,6 +22,8 @@
|
||||
input[type=range]::-webkit-slider-runnable-track {
|
||||
width: 100%; height: 4px; cursor: pointer; background: #e5e7eb; border-radius: 2px;
|
||||
}
|
||||
.dark input[type=range]::-webkit-slider-thumb { background: #e8e8e8; }
|
||||
.dark input[type=range]::-webkit-slider-runnable-track { background: #333333; }
|
||||
|
||||
/* Ingredient Active States */
|
||||
.ingredient-active .check-box,
|
||||
@@ -30,14 +32,151 @@
|
||||
.ingredient-active .rd-check-icon { display: block; }
|
||||
.ingredient-active .ingredient-text,
|
||||
.ingredient-active .rd-ing-text { text-decoration: line-through; color: #9ca3af; }
|
||||
.dark .ingredient-active .check-box,
|
||||
.dark .ingredient-active .rd-check-box { background-color: #f0f0f0; border-color: #f0f0f0; }
|
||||
|
||||
/* Utilities */
|
||||
.no-scrollbar::-webkit-scrollbar { display: none; }
|
||||
.no-scrollbar { -ms-overflow-style: none; scrollbar-width: none; }
|
||||
.sr-only { position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip: rect(0,0,0,0); white-space: nowrap; border-width: 0; }
|
||||
|
||||
/* ===== Dark Mode Overrides ===== */
|
||||
.dark { color-scheme: dark; }
|
||||
|
||||
/* --- Gray backgrounds --- */
|
||||
.dark .bg-white { background-color: #1a1a1a !important; }
|
||||
.dark .bg-gray-50 { background-color: #0d0d0d !important; }
|
||||
.dark .bg-gray-100 { background-color: #242424 !important; }
|
||||
.dark .bg-gray-200 { background-color: #2e2e2e !important; }
|
||||
.dark .bg-gray-900 { background-color: #f0f0f0 !important; }
|
||||
.dark .bg-gray-900.text-white,
|
||||
.dark .bg-gray-900 .text-white { color: #0d0d0d !important; }
|
||||
.dark .border-gray-900 { border-color: #f0f0f0 !important; }
|
||||
|
||||
/* Semi-transparent backgrounds */
|
||||
.dark .bg-white\/90 { background-color: rgba(26, 26, 26, 0.92) !important; }
|
||||
.dark .bg-white\/80 { background-color: rgba(26, 26, 26, 0.85) !important; }
|
||||
.dark .bg-gray-50\/80 { background-color: rgba(20, 20, 20, 0.85) !important; }
|
||||
.dark .bg-gray-50\/90 { background-color: rgba(20, 20, 20, 0.92) !important; }
|
||||
|
||||
/* --- Gray text --- */
|
||||
.dark .text-gray-900 { color: #f5f5f5 !important; }
|
||||
.dark .text-gray-800 { color: #e8e8e8 !important; }
|
||||
.dark .text-gray-700 { color: #d4d4d4 !important; }
|
||||
.dark .text-gray-600 { color: #a0a0a0 !important; }
|
||||
.dark .text-gray-500 { color: #888888 !important; }
|
||||
.dark .text-gray-400 { color: #666666 !important; }
|
||||
.dark .text-gray-300 { color: #444444 !important; }
|
||||
.dark .text-black { color: #ffffff !important; }
|
||||
|
||||
/* --- Gray borders --- */
|
||||
.dark .border-gray-200 { border-color: #2a2a2a !important; }
|
||||
.dark .border-gray-100 { border-color: #1f1f1f !important; }
|
||||
.dark .border-gray-300 { border-color: #333333 !important; }
|
||||
|
||||
/* --- Dividers --- */
|
||||
.dark .divide-gray-50 > :not([hidden]) ~ :not([hidden]) { border-color: #1a1a1a !important; }
|
||||
.dark .divide-gray-100 > :not([hidden]) ~ :not([hidden]) { border-color: #1f1f1f !important; }
|
||||
.dark .divide-gray-200 > :not([hidden]) ~ :not([hidden]) { border-color: #2a2a2a !important; }
|
||||
.dark .divide-red-50 > :not([hidden]) ~ :not([hidden]) { border-color: #2a1a1a !important; }
|
||||
|
||||
/* --- Ring utilities --- */
|
||||
.dark .ring-gray-200 { --tw-ring-color: #2a2a2a !important; }
|
||||
.dark .ring-gray-900 { --tw-ring-color: #f0f0f0 !important; }
|
||||
|
||||
/* --- Hover states --- */
|
||||
.dark .hover\:bg-gray-50:hover { background-color: #1f1f1f !important; }
|
||||
.dark .hover\:bg-gray-100:hover { background-color: #2e2e2e !important; }
|
||||
.dark .hover\:bg-white:hover { background-color: #242424 !important; }
|
||||
.dark .hover\:text-gray-700:hover { color: #e8e8e8 !important; }
|
||||
.dark .hover\:text-gray-900:hover { color: #f5f5f5 !important; }
|
||||
.dark .hover\:text-black:hover { color: #ffffff !important; }
|
||||
.dark .hover\:bg-black:hover { background-color: #e0e0e0 !important; color: #0d0d0d !important; }
|
||||
.dark .hover\:bg-red-50:hover { background-color: #2a1a1a !important; }
|
||||
.dark .hover\:border-gray-300:hover { border-color: #444444 !important; }
|
||||
.dark .hover\:border-gray-400:hover { border-color: #555555 !important; }
|
||||
.dark .hover\:border-gray-900:hover { border-color: #f0f0f0 !important; }
|
||||
.dark .hover\:border-red-200:hover { border-color: #5c2020 !important; }
|
||||
|
||||
/* --- Focus --- */
|
||||
.dark .focus\:border-gray-400:focus { border-color: #555555 !important; }
|
||||
.dark .focus\:border-gray-300:focus { border-color: #444444 !important; }
|
||||
|
||||
/* --- Shadows --- */
|
||||
.dark .shadow-lg { box-shadow: 0 10px 15px -3px rgba(0,0,0,0.5), 0 4px 6px -4px rgba(0,0,0,0.4) !important; }
|
||||
.dark .shadow-sm { box-shadow: 0 1px 3px 0 rgba(0,0,0,0.4) !important; }
|
||||
.dark .border-dashed { border-color: #333333 !important; }
|
||||
|
||||
/* --- Inputs --- */
|
||||
.dark input, .dark select, .dark textarea {
|
||||
background-color: #1a1a1a !important;
|
||||
color: #e8e8e8 !important;
|
||||
border-color: #2a2a2a !important;
|
||||
}
|
||||
.dark input::placeholder, .dark textarea::placeholder {
|
||||
color: #666666 !important;
|
||||
}
|
||||
|
||||
/* --- Toast --- */
|
||||
.dark #app-toast .rounded-xl,
|
||||
.dark #planner-toast .rounded-xl {
|
||||
background-color: #f0f0f0 !important;
|
||||
color: #0d0d0d !important;
|
||||
}
|
||||
|
||||
/* --- Amber / Summary card --- */
|
||||
.dark #planner-summary-card {
|
||||
background: #1e1a10 !important;
|
||||
border-color: #4a3800 !important;
|
||||
}
|
||||
.dark .bg-amber-50 { background-color: #1e1a10 !important; }
|
||||
.dark .bg-amber-50\/30 { background-color: rgba(60, 45, 0, 0.25) !important; }
|
||||
.dark .bg-amber-50\/50 { background-color: rgba(80, 60, 0, 0.2) !important; }
|
||||
.dark .bg-amber-100\/50 { background-color: rgba(80, 60, 0, 0.2) !important; }
|
||||
.dark .hover\:bg-amber-100\/50:hover { background-color: rgba(80, 60, 0, 0.35) !important; }
|
||||
.dark .border-amber-200 { border-color: #4a3800 !important; }
|
||||
.dark .border-amber-200\/80 { border-color: #4a3800 !important; }
|
||||
.dark .border-amber-200\/60 { border-color: #3d2e00 !important; }
|
||||
.dark .border-amber-100 { border-color: #332600 !important; }
|
||||
.dark .text-amber-900\/70 { color: #fbbf24 !important; }
|
||||
.dark .text-amber-900\/80 { color: #fbbf24 !important; }
|
||||
.dark .text-amber-900 { color: #fcd34d !important; }
|
||||
.dark .text-amber-600 { color: #f59e0b !important; }
|
||||
.dark .text-amber-500 { color: #f59e0b !important; }
|
||||
.dark .divide-amber-100\/80 > :not([hidden]) ~ :not([hidden]) { border-color: #332600 !important; }
|
||||
|
||||
/* --- Emerald accents --- */
|
||||
.dark .bg-emerald-50 { background-color: #0a1f15 !important; }
|
||||
.dark .bg-emerald-100 { background-color: #064e3b !important; }
|
||||
.dark .bg-emerald-500 { background-color: #10b981 !important; }
|
||||
.dark .bg-emerald-600 { background-color: #059669 !important; }
|
||||
.dark .hover\:bg-emerald-700:hover { background-color: #047857 !important; }
|
||||
.dark .hover\:bg-emerald-100\/80:hover { background-color: rgba(6, 78, 59, 0.5) !important; }
|
||||
.dark .border-emerald-200\/80 { border-color: #065f46 !important; }
|
||||
.dark .text-emerald-800 { color: #6ee7b7 !important; }
|
||||
.dark .text-emerald-600 { color: #34d399 !important; }
|
||||
.dark .text-emerald-600\/80 { color: rgba(52, 211, 153, 0.85) !important; }
|
||||
.dark .text-emerald-500 { color: #34d399 !important; }
|
||||
|
||||
/* --- Red accents --- */
|
||||
.dark .bg-red-50 { background-color: #1f0a0a !important; }
|
||||
.dark .bg-red-100 { background-color: #450a0a !important; }
|
||||
.dark .bg-red-500 { background-color: #ef4444 !important; }
|
||||
.dark .border-red-200\/80 { border-color: #5c2020 !important; }
|
||||
.dark .border-red-100\/80 { border-color: #3b1010 !important; }
|
||||
.dark .text-red-800 { color: #fca5a5 !important; }
|
||||
.dark .text-red-600 { color: #f87171 !important; }
|
||||
.dark .text-red-600\/80 { color: rgba(248, 113, 113, 0.85) !important; }
|
||||
.dark .text-red-500 { color: #f87171 !important; }
|
||||
.dark .text-red-400 { color: #fb7185 !important; }
|
||||
.dark .hover\:text-red-600:hover { color: #f87171 !important; }
|
||||
.dark .bg-amber-500 { background-color: #f59e0b !important; }
|
||||
</style>
|
||||
</head>
|
||||
<body class="m-0 min-h-dvh bg-white font-sans text-gray-800">
|
||||
<script>
|
||||
if (localStorage.getItem('theme') === 'dark') document.documentElement.classList.add('dark');
|
||||
</script>
|
||||
|
||||
<div id="app-container" class="relative flex h-dvh min-h-0 w-full flex-col overflow-hidden bg-white">
|
||||
</div>
|
||||
|
||||
40
js/app.js
40
js/app.js
@@ -3,7 +3,6 @@ import { getFilterHTML, setupFilter } from './views/Filter.js?v=2';
|
||||
import { getRecipeDetailHTML, setupRecipeDetail } from './views/RecipeDetailV2.js?v=2';
|
||||
import { getMealPlannerHTML, setupMealPlanner } from './views/MealPlanner.js?v=2';
|
||||
import { getPantryHTML, refreshPantry, setupPantry } from './views/Pantry.js?v=2';
|
||||
import { getShoppingHTML, refreshShopping, setupShopping } from './views/Shopping.js?v=2';
|
||||
|
||||
function getAppToastHTML() {
|
||||
return `
|
||||
@@ -14,6 +13,7 @@ function getAppToastHTML() {
|
||||
}
|
||||
|
||||
function getBottomNavHTML() {
|
||||
const isDark = document.documentElement.classList.contains('dark');
|
||||
return `
|
||||
<nav id="app-bottom-nav" class="absolute bottom-0 left-0 right-0 w-full bg-white border-t border-gray-200 flex justify-between px-1 py-2.5 pb-6 z-20 gap-0" aria-label="Główna nawigacja">
|
||||
<button type="button" data-tab="recipes" id="nav-recipes" class="nav-tab flex flex-col items-center gap-0.5 text-black flex-1 min-w-0 max-w-[5.5rem]">
|
||||
@@ -28,21 +28,39 @@ function getBottomNavHTML() {
|
||||
<i class="fas fa-warehouse text-base" aria-hidden="true"></i>
|
||||
<span class="text-[9px] font-medium leading-tight text-center">Spiżarnia</span>
|
||||
</button>
|
||||
<button type="button" data-tab="shopping" id="nav-shopping" class="nav-tab flex flex-col items-center gap-0.5 text-gray-500 hover:text-gray-700 flex-1 min-w-0 max-w-[5.5rem]">
|
||||
<i class="fas fa-cart-shopping text-base" aria-hidden="true"></i>
|
||||
<span class="text-[9px] font-medium leading-tight text-center">Zakupy</span>
|
||||
<button type="button" id="nav-theme-toggle" class="flex flex-col items-center gap-0.5 text-gray-500 hover:text-gray-700 flex-1 min-w-0 max-w-[5.5rem]" aria-label="Przełącz tryb ciemny/jasny">
|
||||
<i class="${isDark ? 'fas fa-sun' : 'fas fa-moon'} text-base" aria-hidden="true"></i>
|
||||
<span class="text-[9px] font-medium leading-tight text-center">${isDark ? 'Jasny' : 'Ciemny'}</span>
|
||||
</button>
|
||||
</nav>
|
||||
`;
|
||||
}
|
||||
|
||||
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');
|
||||
|
||||
const icon = btn.querySelector('i');
|
||||
const label = btn.querySelector('span');
|
||||
if (icon) icon.className = isDark ? 'fas fa-sun text-base' : 'fas fa-moon text-base';
|
||||
if (label) label.textContent = isDark ? 'Jasny' : 'Ciemny';
|
||||
|
||||
const meta = document.querySelector('meta[name="theme-color"]');
|
||||
if (meta) meta.setAttribute('content', isDark ? '#0d0d0d' : '#ffffff');
|
||||
});
|
||||
}
|
||||
|
||||
function setupTabs() {
|
||||
const main = document.getElementById('main-view');
|
||||
const planner = document.getElementById('planner-view');
|
||||
const pantry = document.getElementById('pantry-view');
|
||||
const shopping = document.getElementById('shopping-view');
|
||||
const nav = document.getElementById('app-bottom-nav');
|
||||
if (!main || !planner || !pantry || !shopping || !nav) return;
|
||||
if (!main || !planner || !pantry || !nav) return;
|
||||
|
||||
const activeTab = 'nav-tab flex flex-col items-center gap-0.5 text-black flex-1 min-w-0 max-w-[5.5rem]';
|
||||
const idleTab = 'nav-tab flex flex-col items-center gap-0.5 text-gray-500 hover:text-gray-700 flex-1 min-w-0 max-w-[5.5rem]';
|
||||
@@ -51,15 +69,13 @@ function setupTabs() {
|
||||
main.classList.toggle('hidden', tab !== 'recipes');
|
||||
planner.classList.toggle('hidden', tab !== 'planner');
|
||||
pantry.classList.toggle('hidden', tab !== 'pantry');
|
||||
shopping.classList.toggle('hidden', tab !== 'shopping');
|
||||
|
||||
if (tab === 'pantry') refreshPantry();
|
||||
if (tab === 'shopping') refreshShopping();
|
||||
|
||||
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' || id === 'shopping') {
|
||||
if (id === 'recipes' || id === 'planner' || id === 'pantry') {
|
||||
btn.className = id === tab ? activeTab : idleTab;
|
||||
}
|
||||
});
|
||||
@@ -69,14 +85,13 @@ function setupTabs() {
|
||||
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' || tab === 'shopping') apply(tab);
|
||||
if (tab === 'recipes' || tab === 'planner' || tab === 'pantry') apply(tab);
|
||||
});
|
||||
|
||||
apply('recipes');
|
||||
|
||||
window.refreshStockViews = () => {
|
||||
refreshPantry();
|
||||
refreshShopping();
|
||||
};
|
||||
}
|
||||
|
||||
@@ -87,7 +102,6 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
${getRecipeListHTML()}
|
||||
${getMealPlannerHTML()}
|
||||
${getPantryHTML()}
|
||||
${getShoppingHTML()}
|
||||
${getBottomNavHTML()}
|
||||
${getRecipeDetailHTML()}
|
||||
${getFilterHTML()}
|
||||
@@ -95,10 +109,10 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
`;
|
||||
|
||||
setupTabs();
|
||||
setupThemeToggle();
|
||||
setupRecipeList();
|
||||
setupMealPlanner();
|
||||
setupPantry();
|
||||
setupShopping();
|
||||
setupFilter();
|
||||
setupRecipeDetail();
|
||||
});
|
||||
|
||||
@@ -1,290 +0,0 @@
|
||||
import {
|
||||
addFreeformLine,
|
||||
addFreeformList,
|
||||
applyCheckedKitchenListToPantry,
|
||||
categoryLabel,
|
||||
clearCheckedInList,
|
||||
deleteList,
|
||||
getActiveList,
|
||||
getListSummaries,
|
||||
KITCHEN_LIST_ID,
|
||||
removeItemFromList,
|
||||
setActiveListId,
|
||||
toggleItemInList,
|
||||
updateKitchenItemAmount,
|
||||
} from '../services/pantryShopping.js';
|
||||
import { showAppToast } from '../ui/toast.js';
|
||||
|
||||
function escapeHtml(s) {
|
||||
return String(s)
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"');
|
||||
}
|
||||
|
||||
export function getShoppingHTML() {
|
||||
return `
|
||||
<div id="shopping-view" class="hidden flex flex-col h-full absolute inset-0 overflow-hidden bg-gray-50 z-10 pb-24">
|
||||
<div class="shrink-0 bg-white border-b border-gray-200 mt-3 px-4 pt-2 pb-3 space-y-3">
|
||||
<div class="flex gap-2 items-center">
|
||||
<label class="sr-only" for="shopping-list-select">Aktywna lista</label>
|
||||
<select id="shopping-list-select" class="flex-1 min-w-0 rounded-xl border border-gray-200 bg-gray-50 px-3 py-2.5 text-sm font-medium text-gray-900 outline-none focus:border-gray-400"></select>
|
||||
<button type="button" id="shopping-new-list" class="shrink-0 w-10 h-10 rounded-xl border border-gray-200 bg-white text-gray-700 hover:bg-gray-50 flex items-center justify-center" title="Nowa lista dowolna" aria-label="Nowa lista dowolna">
|
||||
<i class="fas fa-plus text-sm"></i>
|
||||
</button>
|
||||
</div>
|
||||
<button type="button" id="shopping-delete-list" class="hidden w-full py-2 rounded-lg text-xs font-medium text-red-600 hover:bg-red-50 transition-colors">
|
||||
Usuń tę listę (nie dotyczy listy kuchennej)
|
||||
</button>
|
||||
<div id="shopping-kitchen-actions" class="hidden flex gap-2">
|
||||
<button type="button" id="shopping-to-pantry" class="flex-1 py-2.5 rounded-xl bg-emerald-600 hover:bg-emerald-700 text-white text-xs font-semibold transition-colors flex items-center justify-center gap-1.5">
|
||||
<i class="fas fa-warehouse text-[10px]"></i> Kupione → spiżarnia
|
||||
</button>
|
||||
<button type="button" id="shopping-clear-checked" class="flex-1 py-2.5 rounded-xl border border-gray-200 bg-white text-gray-700 hover:bg-gray-50 text-xs font-semibold transition-colors flex items-center justify-center gap-1.5">
|
||||
<i class="fas fa-broom text-[10px]"></i> Wyczyść kupione
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="shopping-freeform-add" class="hidden shrink-0 px-4 pt-3 pb-2 space-y-2 border-b border-gray-100">
|
||||
<div class="flex gap-2">
|
||||
<input type="text" id="shopping-freeform-input" class="flex-1 rounded-xl border border-gray-200 px-3 py-2.5 text-sm outline-none focus:border-gray-400" placeholder="Co kupić?" maxlength="200" />
|
||||
<button type="button" id="shopping-freeform-submit" class="shrink-0 px-4 py-2.5 rounded-xl bg-gray-900 text-white text-xs font-semibold hover:bg-black">Dodaj</button>
|
||||
</div>
|
||||
<input type="text" id="shopping-freeform-note" class="w-full rounded-lg border border-gray-100 px-3 py-2 text-xs text-gray-600 outline-none focus:border-gray-300" placeholder="Opcjonalna notatka (ilość, sklep…)" maxlength="120" />
|
||||
</div>
|
||||
|
||||
<div id="shopping-scroll" class="flex-1 overflow-y-auto px-4 py-3 pb-4 no-scrollbar">
|
||||
<div id="shopping-list-root" class="space-y-4"></div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
function syncListSelect() {
|
||||
const sel = document.getElementById('shopping-list-select');
|
||||
if (!sel) return;
|
||||
const { lists, activeListId } = getListSummaries();
|
||||
sel.innerHTML = lists.map((l) => {
|
||||
const suffix = l.openCount ? ` (${l.openCount})` : '';
|
||||
const label = `${l.name}${suffix}`;
|
||||
return `<option value="${escapeHtml(l.id)}">${escapeHtml(label)}</option>`;
|
||||
}).join('');
|
||||
sel.value = activeListId;
|
||||
}
|
||||
|
||||
function syncChromeForList() {
|
||||
const list = getActiveList();
|
||||
const isKitchen = list.type === 'kitchen';
|
||||
const delBtn = document.getElementById('shopping-delete-list');
|
||||
const ffAdd = document.getElementById('shopping-freeform-add');
|
||||
const kitchenActions = document.getElementById('shopping-kitchen-actions');
|
||||
|
||||
if (ffAdd) ffAdd.classList.toggle('hidden', isKitchen);
|
||||
if (delBtn) delBtn.classList.toggle('hidden', isKitchen);
|
||||
|
||||
if (kitchenActions) {
|
||||
const hasChecked = isKitchen && list.items.some((i) => i.checked);
|
||||
kitchenActions.classList.toggle('hidden', !hasChecked);
|
||||
}
|
||||
}
|
||||
|
||||
function renderKitchenItems() {
|
||||
const root = document.getElementById('shopping-list-root');
|
||||
if (!root) return;
|
||||
|
||||
const list = getActiveList();
|
||||
if (list.type !== 'kitchen') return;
|
||||
|
||||
const items = list.items;
|
||||
if (items.length === 0) {
|
||||
root.innerHTML = '<p class="text-sm text-gray-500 text-center py-10">Brak pozycji.</p>';
|
||||
return;
|
||||
}
|
||||
|
||||
const groups = {};
|
||||
for (const it of items) {
|
||||
const c = it.category || 'inne';
|
||||
if (!groups[c]) groups[c] = [];
|
||||
groups[c].push(it);
|
||||
}
|
||||
|
||||
root.innerHTML = Object.keys(groups)
|
||||
.sort((a, b) => categoryLabel(a).localeCompare(categoryLabel(b)))
|
||||
.map((cat) => `
|
||||
<div class="rounded-xl border border-gray-200 bg-white overflow-hidden shadow-sm">
|
||||
<p class="text-[10px] font-bold text-gray-400 uppercase tracking-wide px-3 py-2 bg-gray-50 border-b border-gray-100">${escapeHtml(categoryLabel(cat))}</p>
|
||||
<ul class="divide-y divide-gray-100">
|
||||
${groups[cat].map((it) => `
|
||||
<li class="flex items-start gap-3 px-3 py-3 ${it.checked ? 'opacity-60' : ''}">
|
||||
<button type="button" data-shop-toggle="${escapeHtml(it.id)}" class="mt-0.5 w-5 h-5 rounded border shrink-0 flex items-center justify-center transition-colors ${it.checked ? 'bg-gray-900 border-gray-900 text-white' : 'border-gray-300 bg-white'}" aria-label="Kupione">
|
||||
${it.checked ? '<i class="fas fa-check text-[10px]"></i>' : ''}
|
||||
</button>
|
||||
<div class="min-w-0 flex-1">
|
||||
<p class="text-sm font-medium text-gray-900 ${it.checked ? 'line-through text-gray-500' : ''}">${escapeHtml(it.name)}</p>
|
||||
<button type="button" data-shop-edit-amount="${escapeHtml(it.id)}" data-current-amount="${it.amount}" data-unit="${escapeHtml(it.unit)}" class="text-xs text-gray-600 tabular-nums mt-0.5 hover:text-gray-900 underline decoration-dashed underline-offset-2 cursor-pointer">${escapeHtml(String(it.amount))} ${escapeHtml(it.unit)}</button>
|
||||
${it.sourceNote ? `<p class="text-[10px] text-gray-400 mt-1">${escapeHtml(it.sourceNote)}</p>` : ''}
|
||||
</div>
|
||||
<button type="button" data-shop-remove="${escapeHtml(it.id)}" class="shrink-0 w-8 h-8 rounded-full text-gray-400 hover:text-red-600 hover:bg-red-50 transition-colors" aria-label="Usuń">
|
||||
<i class="fas fa-times text-xs"></i>
|
||||
</button>
|
||||
</li>`).join('')}
|
||||
</ul>
|
||||
</div>
|
||||
`)
|
||||
.join('');
|
||||
|
||||
bindItemButtons(list.id);
|
||||
}
|
||||
|
||||
function renderFreeformItems() {
|
||||
const root = document.getElementById('shopping-list-root');
|
||||
if (!root) return;
|
||||
|
||||
const list = getActiveList();
|
||||
if (list.type !== 'freeform') return;
|
||||
|
||||
const items = list.items;
|
||||
if (items.length === 0) {
|
||||
root.innerHTML = '<p class="text-sm text-gray-500 text-center py-10">Dodaj dowolny tekst powyżej — bez powiązania z katalogiem składników.</p>';
|
||||
return;
|
||||
}
|
||||
|
||||
root.innerHTML = `
|
||||
<div class="rounded-xl border border-gray-200 bg-white overflow-hidden shadow-sm">
|
||||
<ul class="divide-y divide-gray-100">
|
||||
${items.map((it) => `
|
||||
<li class="flex items-start gap-3 px-3 py-3 ${it.checked ? 'opacity-60' : ''}">
|
||||
<button type="button" data-shop-toggle="${escapeHtml(it.id)}" class="mt-0.5 w-5 h-5 rounded border shrink-0 flex items-center justify-center transition-colors ${it.checked ? 'bg-gray-900 border-gray-900 text-white' : 'border-gray-300 bg-white'}">
|
||||
${it.checked ? '<i class="fas fa-check text-[10px]"></i>' : ''}
|
||||
</button>
|
||||
<div class="min-w-0 flex-1">
|
||||
<p class="text-sm font-medium text-gray-900 ${it.checked ? 'line-through text-gray-500' : ''}">${escapeHtml(it.text)}</p>
|
||||
${it.note ? `<p class="text-xs text-gray-500 mt-1">${escapeHtml(it.note)}</p>` : ''}
|
||||
</div>
|
||||
<button type="button" data-shop-remove="${escapeHtml(it.id)}" class="shrink-0 w-8 h-8 rounded-full text-gray-400 hover:text-red-600 hover:bg-red-50 transition-colors" aria-label="Usuń">
|
||||
<i class="fas fa-times text-xs"></i>
|
||||
</button>
|
||||
</li>`).join('')}
|
||||
</ul>
|
||||
</div>`;
|
||||
|
||||
bindItemButtons(list.id);
|
||||
}
|
||||
|
||||
function bindItemButtons(listId) {
|
||||
const root = document.getElementById('shopping-list-root');
|
||||
if (!root) return;
|
||||
|
||||
root.querySelectorAll('[data-shop-toggle]').forEach((btn) => {
|
||||
btn.addEventListener('click', () => {
|
||||
const id = btn.getAttribute('data-shop-toggle');
|
||||
if (id) toggleItemInList(listId, id);
|
||||
refreshShopping();
|
||||
});
|
||||
});
|
||||
root.querySelectorAll('[data-shop-remove]').forEach((btn) => {
|
||||
btn.addEventListener('click', () => {
|
||||
const id = btn.getAttribute('data-shop-remove');
|
||||
if (id) removeItemFromList(listId, id);
|
||||
refreshShopping();
|
||||
});
|
||||
});
|
||||
root.querySelectorAll('[data-shop-edit-amount]').forEach((btn) => {
|
||||
btn.addEventListener('click', (e) => {
|
||||
e.stopPropagation();
|
||||
const itemId = btn.getAttribute('data-shop-edit-amount');
|
||||
const current = btn.getAttribute('data-current-amount');
|
||||
const unit = btn.getAttribute('data-unit');
|
||||
const newVal = window.prompt(`Nowa ilość (${unit}):`, current);
|
||||
if (newVal === null) return;
|
||||
const num = parseFloat(newVal.replace(',', '.'));
|
||||
if (!Number.isFinite(num) || num < 0) {
|
||||
showAppToast('Nieprawidłowa wartość.');
|
||||
return;
|
||||
}
|
||||
updateKitchenItemAmount(listId, itemId, num);
|
||||
refreshShopping();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
export function refreshShopping() {
|
||||
syncListSelect();
|
||||
syncChromeForList();
|
||||
const list = getActiveList();
|
||||
if (list.type === 'kitchen') renderKitchenItems();
|
||||
else renderFreeformItems();
|
||||
}
|
||||
|
||||
export function setupShopping() {
|
||||
const sel = document.getElementById('shopping-list-select');
|
||||
sel?.addEventListener('change', () => {
|
||||
const v = sel.value;
|
||||
if (v) setActiveListId(v);
|
||||
refreshShopping();
|
||||
});
|
||||
|
||||
document.getElementById('shopping-new-list')?.addEventListener('click', () => {
|
||||
const name = window.prompt('Nazwa nowej listy (dowolne zakupy):', 'Nowa lista');
|
||||
if (name === null) return;
|
||||
addFreeformList(name);
|
||||
showAppToast('Utworzono listę.');
|
||||
refreshShopping();
|
||||
});
|
||||
|
||||
document.getElementById('shopping-delete-list')?.addEventListener('click', () => {
|
||||
const list = getActiveList();
|
||||
if (list.id === KITCHEN_LIST_ID) return;
|
||||
if (!window.confirm(`Usunąć listę „${list.name}”?`)) return;
|
||||
deleteList(list.id);
|
||||
showAppToast('Lista usunięta.');
|
||||
refreshShopping();
|
||||
});
|
||||
|
||||
const submitFreeform = () => {
|
||||
const list = getActiveList();
|
||||
if (list.type !== 'freeform') return;
|
||||
const input = document.getElementById('shopping-freeform-input');
|
||||
const note = document.getElementById('shopping-freeform-note');
|
||||
const text = input?.value || '';
|
||||
addFreeformLine(list.id, text, note?.value || '');
|
||||
if (input) input.value = '';
|
||||
if (note) note.value = '';
|
||||
refreshShopping();
|
||||
};
|
||||
|
||||
document.getElementById('shopping-freeform-submit')?.addEventListener('click', submitFreeform);
|
||||
document.getElementById('shopping-freeform-input')?.addEventListener('keydown', (e) => {
|
||||
if (e.key === 'Enter') {
|
||||
e.preventDefault();
|
||||
submitFreeform();
|
||||
}
|
||||
});
|
||||
|
||||
document.getElementById('shopping-to-pantry')?.addEventListener('click', () => {
|
||||
const list = getActiveList();
|
||||
if (list.type !== 'kitchen') return;
|
||||
const checked = list.items.filter((i) => i.checked);
|
||||
if (checked.length === 0) return;
|
||||
const preview = checked.map((i) => ` • ${i.name}: ${i.amount} ${i.unit}`).join('\n');
|
||||
if (!window.confirm(`Przenieść do spiżarni?\n\n${preview}`)) return;
|
||||
applyCheckedKitchenListToPantry();
|
||||
showAppToast(`Przeniesiono ${checked.length} pozycji do spiżarni.`);
|
||||
window.refreshPantry?.();
|
||||
refreshShopping();
|
||||
});
|
||||
|
||||
document.getElementById('shopping-clear-checked')?.addEventListener('click', () => {
|
||||
const list = getActiveList();
|
||||
const checkedCount = list.items.filter((i) => i.checked).length;
|
||||
if (checkedCount === 0) return;
|
||||
clearCheckedInList(list.id);
|
||||
showAppToast(`Usunięto ${checkedCount} kupionych pozycji.`);
|
||||
refreshShopping();
|
||||
});
|
||||
|
||||
refreshShopping();
|
||||
window.refreshShopping = refreshShopping;
|
||||
}
|
||||
Reference in New Issue
Block a user