Add recipe mockup
This commit is contained in:
104
stacks/recipe/js/app.js
Normal file
104
stacks/recipe/js/app.js
Normal file
@@ -0,0 +1,104 @@
|
||||
import { getRecipeListHTML } from './views/RecipeList.js';
|
||||
import { getFilterHTML, setupFilter } from './views/Filter.js';
|
||||
import { getRecipeDetailHTML, setupRecipeDetail } from './views/RecipeDetail.js';
|
||||
import { getMealPlannerHTML, setupMealPlanner } from './views/MealPlanner.js';
|
||||
|
||||
function getBottomNavHTML() {
|
||||
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-4 py-3 pb-6 z-20" aria-label="Główna nawigacja">
|
||||
<button type="button" data-tab="recipes" id="nav-recipes" class="nav-tab flex flex-col items-center gap-1 text-black min-w-[3.5rem]">
|
||||
<i class="fas fa-book text-xl" aria-hidden="true"></i>
|
||||
<span class="text-[11px] font-medium">Przepisy</span>
|
||||
</button>
|
||||
<button type="button" data-tab="planner" id="nav-planner" class="nav-tab flex flex-col items-center gap-1 text-gray-500 hover:text-gray-700 min-w-[3.5rem]">
|
||||
<i class="far fa-calendar-alt text-xl" aria-hidden="true"></i>
|
||||
<span class="text-[11px] font-medium">Planer</span>
|
||||
</button>
|
||||
<button type="button" data-tab="shopping" class="nav-tab flex flex-col items-center gap-1 text-gray-500 hover:text-gray-700 min-w-[3.5rem]" disabled title="Wkrótce">
|
||||
<i class="fas fa-shopping-cart text-xl" aria-hidden="true"></i>
|
||||
<span class="text-[11px] font-medium">Zakupy</span>
|
||||
</button>
|
||||
<button type="button" data-tab="pantry" class="nav-tab flex flex-col items-center gap-1 text-gray-500 hover:text-gray-700 min-w-[3.5rem]" disabled title="Wkrótce">
|
||||
<i class="fas fa-box text-xl" aria-hidden="true"></i>
|
||||
<span class="text-[11px] font-medium">Zapasy</span>
|
||||
</button>
|
||||
</nav>
|
||||
`;
|
||||
}
|
||||
|
||||
function setupTabs() {
|
||||
const main = document.getElementById('main-view');
|
||||
const planner = document.getElementById('planner-view');
|
||||
const nav = document.getElementById('app-bottom-nav');
|
||||
if (!main || !planner || !nav) return;
|
||||
|
||||
const activeTab = 'nav-tab flex flex-col items-center gap-1 text-black min-w-[3.5rem]';
|
||||
const idleTab = 'nav-tab flex flex-col items-center gap-1 text-gray-500 hover:text-gray-700 min-w-[3.5rem]';
|
||||
|
||||
const apply = (tab) => {
|
||||
const showRecipes = tab === 'recipes';
|
||||
main.classList.toggle('hidden', !showRecipes);
|
||||
planner.classList.toggle('hidden', showRecipes);
|
||||
|
||||
nav.querySelectorAll('.nav-tab[data-tab]').forEach((btn) => {
|
||||
const id = btn.getAttribute('data-tab');
|
||||
if (btn.hasAttribute('disabled')) return;
|
||||
if (id === 'recipes' || id === 'planner') {
|
||||
btn.className = id === tab ? activeTab : idleTab;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
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') apply(tab);
|
||||
});
|
||||
|
||||
apply('recipes');
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const appContainer = document.getElementById('app-container');
|
||||
|
||||
appContainer.innerHTML = `
|
||||
${getRecipeListHTML()}
|
||||
${getMealPlannerHTML()}
|
||||
${getBottomNavHTML()}
|
||||
${getRecipeDetailHTML()}
|
||||
${getFilterHTML()}
|
||||
`;
|
||||
|
||||
setupTabs();
|
||||
setupMealPlanner();
|
||||
setupFilter();
|
||||
setupRecipeDetail();
|
||||
});
|
||||
|
||||
// --- GLOBAL NAVIGATION METHODS ---
|
||||
window.openRecipeDetail = () => {
|
||||
const view = document.getElementById('recipe-detail-view');
|
||||
// Swap Tailwind classes to slide IN
|
||||
view.classList.remove('translate-x-full', 'opacity-0', 'pointer-events-none');
|
||||
view.classList.add('translate-x-0', 'opacity-100', 'pointer-events-auto');
|
||||
};
|
||||
|
||||
window.closeRecipeDetail = () => {
|
||||
const view = document.getElementById('recipe-detail-view');
|
||||
// Swap Tailwind classes to slide OUT
|
||||
view.classList.remove('translate-x-0', 'opacity-100', 'pointer-events-auto');
|
||||
view.classList.add('translate-x-full', 'opacity-0', 'pointer-events-none');
|
||||
};
|
||||
|
||||
window.openFilters = () => {
|
||||
const fv = document.getElementById('filter-view');
|
||||
fv.classList.remove('hidden');
|
||||
fv.classList.add('flex');
|
||||
};
|
||||
|
||||
window.closeFilters = () => {
|
||||
const fv = document.getElementById('filter-view');
|
||||
fv.classList.add('hidden');
|
||||
fv.classList.remove('flex');
|
||||
};
|
||||
63
stacks/recipe/js/views/Filter.js
Normal file
63
stacks/recipe/js/views/Filter.js
Normal file
@@ -0,0 +1,63 @@
|
||||
export function getFilterHTML() {
|
||||
return `
|
||||
<div id="filter-view" class="absolute inset-0 bg-white z-50 hidden flex-col">
|
||||
<div class="p-4 border-b border-gray-200 flex items-center justify-between mt-4">
|
||||
<button onclick="closeFilters()" class="w-10 h-10 flex items-center justify-center text-gray-600 hover:bg-gray-100 rounded-full transition-colors"><i class="fas fa-arrow-left text-lg"></i></button>
|
||||
<h2 class="text-lg font-semibold text-black">Filtry</h2>
|
||||
<button class="px-2 text-sm font-medium text-gray-500 hover:text-black transition-colors">Wyczyść</button>
|
||||
</div>
|
||||
|
||||
<div class="flex-1 overflow-y-auto p-6 space-y-8">
|
||||
<div>
|
||||
<h3 class="text-base font-semibold text-black mb-4">Pora posiłku</h3>
|
||||
<div class="flex flex-wrap gap-2.5">
|
||||
<button class="px-4 py-2 bg-gray-900 text-white rounded-full text-sm font-medium transition-colors">Śniadanie</button>
|
||||
<button class="px-4 py-2 bg-gray-100 text-gray-700 hover:bg-gray-200 rounded-full text-sm font-medium transition-colors">Drugie śniadanie</button>
|
||||
<button class="px-4 py-2 bg-gray-100 text-gray-700 hover:bg-gray-200 rounded-full text-sm font-medium transition-colors">Obiad</button>
|
||||
<button class="px-4 py-2 bg-gray-100 text-gray-700 hover:bg-gray-200 rounded-full text-sm font-medium transition-colors">Podwieczorek</button>
|
||||
<button class="px-4 py-2 bg-gray-100 text-gray-700 hover:bg-gray-200 rounded-full text-sm font-medium transition-colors">Kolacja</button>
|
||||
<button class="px-4 py-2 bg-gray-100 text-gray-700 hover:bg-gray-200 rounded-full text-sm font-medium transition-colors">Przekąska</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h3 class="text-base font-semibold text-black mb-4">Dieta i tagi</h3>
|
||||
<div class="flex flex-wrap gap-2.5">
|
||||
<button class="px-4 py-2 bg-gray-100 text-gray-700 hover:bg-gray-200 rounded-full text-sm font-medium transition-colors">Wegetariańska</button>
|
||||
<button class="px-4 py-2 bg-gray-100 text-gray-700 hover:bg-gray-200 rounded-full text-sm font-medium transition-colors">Wegańska</button>
|
||||
<button class="px-4 py-2 bg-gray-900 text-white rounded-full text-sm font-medium transition-colors">Wysokobiałkowe</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div class="flex justify-between items-center mb-4">
|
||||
<h3 class="text-base font-semibold text-black">Maks. czas przygotowania</h3>
|
||||
<span id="time-display" class="text-sm font-medium text-gray-600">30 min</span>
|
||||
</div>
|
||||
<div class="px-1">
|
||||
<input type="range" id="prep-time-slider" min="5" max="120" step="5" value="30" class="w-full appearance-none bg-transparent">
|
||||
<div class="flex justify-between text-xs text-gray-400 mt-3 font-medium">
|
||||
<span>5 min</span><span>30 min</span><span>1 godz.</span><span>2 godz.+</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="p-4 border-t border-gray-200 bg-white mt-auto">
|
||||
<button onclick="closeFilters()" class="w-full bg-gray-900 hover:bg-black text-white py-3.5 rounded-xl font-semibold shadow-sm transition-colors text-sm">Pokaż 12 wyników</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
export function setupFilter() {
|
||||
const timeSlider = document.getElementById('prep-time-slider');
|
||||
const timeDisplay = document.getElementById('time-display');
|
||||
|
||||
if(timeSlider) {
|
||||
timeSlider.addEventListener('input', (e) => {
|
||||
const val = e.target.value;
|
||||
timeDisplay.textContent = val >= 120 ? 'ponad 120 min' : `${val} min`;
|
||||
});
|
||||
}
|
||||
}
|
||||
307
stacks/recipe/js/views/MealPlanner.js
Normal file
307
stacks/recipe/js/views/MealPlanner.js
Normal file
@@ -0,0 +1,307 @@
|
||||
const MONTHS_SHORT = [
|
||||
'sty', 'lut', 'mar', 'kwi', 'maj', 'cze',
|
||||
'lip', 'sie', 'wrz', 'paź', 'lis', 'gru',
|
||||
];
|
||||
const WEEKDAYS_SHORT = ['pn', 'wt', 'śr', 'cz', 'pt', 'so', 'nd'];
|
||||
|
||||
function startOfDay(d) {
|
||||
const x = new Date(d);
|
||||
x.setHours(0, 0, 0, 0);
|
||||
return x;
|
||||
}
|
||||
|
||||
function sameDay(a, b) {
|
||||
return a.getFullYear() === b.getFullYear()
|
||||
&& a.getMonth() === b.getMonth()
|
||||
&& a.getDate() === b.getDate();
|
||||
}
|
||||
|
||||
function addDays(d, n) {
|
||||
const x = new Date(d);
|
||||
x.setDate(x.getDate() + n);
|
||||
return startOfDay(x);
|
||||
}
|
||||
|
||||
/** Poniedziałek jako pierwszy dzień tygodnia (PL) */
|
||||
function startOfWeekMonday(d) {
|
||||
const date = startOfDay(d);
|
||||
const day = date.getDay();
|
||||
const diff = day === 0 ? -6 : 1 - day;
|
||||
return addDays(date, diff);
|
||||
}
|
||||
|
||||
function startOfMonth(d) {
|
||||
const x = new Date(d.getFullYear(), d.getMonth(), 1);
|
||||
return startOfDay(x);
|
||||
}
|
||||
|
||||
function addMonths(d, n) {
|
||||
const x = new Date(d);
|
||||
x.setMonth(x.getMonth() + n);
|
||||
return startOfDay(x);
|
||||
}
|
||||
|
||||
function addWeeks(d, n) {
|
||||
return addDays(d, n * 7);
|
||||
}
|
||||
|
||||
function weekContains(weekStart, d) {
|
||||
const t = startOfDay(d).getTime();
|
||||
const ws = weekStart.getTime();
|
||||
const we = addDays(weekStart, 6).getTime();
|
||||
return t >= ws && t <= we;
|
||||
}
|
||||
|
||||
function sameMonth(a, b) {
|
||||
return a.getMonth() === b.getMonth() && a.getFullYear() === b.getFullYear();
|
||||
}
|
||||
|
||||
function isCalendarOnToday(mode, weekStart, monthAnchor, selected) {
|
||||
const today = startOfDay(new Date());
|
||||
if (!sameDay(selected, today)) return false;
|
||||
if (mode === 'week') return weekContains(weekStart, today);
|
||||
return sameMonth(monthAnchor, today);
|
||||
}
|
||||
|
||||
function syncTodayButton(mode, weekStart, monthAnchor, selected) {
|
||||
const btn = document.getElementById('cal-go-today');
|
||||
if (!btn) return;
|
||||
const onToday = isCalendarOnToday(mode, weekStart, monthAnchor, selected);
|
||||
const active = 'h-6 shrink-0 inline-flex items-center justify-center gap-1 rounded-md border border-gray-200 bg-white px-2 text-[10px] font-semibold text-gray-700 shadow-sm hover:bg-gray-50 hover:text-gray-900 transition-colors';
|
||||
const dim = 'h-6 shrink-0 inline-flex items-center justify-center gap-1 rounded-md border border-gray-100 bg-gray-50 px-2 text-[10px] font-semibold text-gray-400 shadow-none cursor-default transition-colors';
|
||||
btn.className = onToday ? dim : active;
|
||||
btn.disabled = onToday;
|
||||
}
|
||||
|
||||
export function getMealPlannerHTML() {
|
||||
return `
|
||||
<div id="planner-view" class="hidden flex flex-col h-full absolute inset-0 bg-gray-50 z-10 pb-24">
|
||||
<div class="shrink-0 bg-white border-b border-gray-200 mt-3">
|
||||
<div class="px-3 pt-2 pb-1.5 flex items-center gap-1">
|
||||
<button type="button" id="cal-prev" class="shrink-0 w-8 h-8 flex items-center justify-center rounded-full border border-gray-200 text-gray-700 hover:bg-gray-50 transition-colors" aria-label="Poprzedni okres">
|
||||
<i class="fas fa-chevron-left text-xs" aria-hidden="true"></i>
|
||||
</button>
|
||||
<p id="cal-period-label" class="flex-1 min-w-0 text-xs font-medium text-gray-900 text-center tabular-nums leading-none px-1 truncate"></p>
|
||||
<button type="button" id="cal-next" class="shrink-0 w-8 h-8 flex items-center justify-center rounded-full border border-gray-200 text-gray-700 hover:bg-gray-50 transition-colors" aria-label="Następny okres">
|
||||
<i class="fas fa-chevron-right text-xs" aria-hidden="true"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div class="px-3 pb-2 flex items-center justify-between gap-3">
|
||||
<button type="button" id="cal-go-today" title="Dziś" aria-label="Przejdź do dzisiejszego dnia"
|
||||
class="h-6 shrink-0 inline-flex items-center justify-center gap-1 rounded-md border border-gray-200 bg-white px-2.5 text-[10px] font-semibold text-gray-700 shadow-sm hover:bg-gray-50 hover:text-gray-900 transition-colors">
|
||||
<i class="fas fa-calendar-day text-[9px] opacity-70" aria-hidden="true"></i>
|
||||
Dziś
|
||||
</button>
|
||||
<div id="planner-cal-mode" class="inline-flex items-center shrink-0 rounded-md border border-gray-200 bg-gray-50 p-px gap-px shadow-sm" role="tablist" aria-label="Skala kalendarza">
|
||||
<button type="button" data-cal-mode="week" id="planner-mode-week" title="Tydzień" aria-label="Widok tygodnia"
|
||||
class="planner-cal-mode-btn w-7 h-6 flex items-center justify-center rounded-[0.3125rem] transition-colors bg-white text-gray-900 shadow-sm">
|
||||
<i class="fas fa-calendar-week text-[10px]" aria-hidden="true"></i>
|
||||
</button>
|
||||
<button type="button" data-cal-mode="month" id="planner-mode-month" title="Miesiąc" aria-label="Widok miesiąca"
|
||||
class="planner-cal-mode-btn w-7 h-6 flex items-center justify-center rounded-[0.3125rem] transition-colors text-gray-400 hover:text-gray-600">
|
||||
<i class="fas fa-calendar-days text-[10px]" aria-hidden="true"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div id="calendar-week-wrap" class="px-3 pb-2.5">
|
||||
<div class="grid grid-cols-7 gap-0.5 text-center text-[9px] font-medium text-gray-400 uppercase tracking-wide mb-0.5 leading-none">
|
||||
${WEEKDAYS_SHORT.map((d) => `<div>${d}</div>`).join('')}
|
||||
</div>
|
||||
<div id="calendar-week-grid" class="grid grid-cols-7 gap-0.5"></div>
|
||||
</div>
|
||||
<div id="calendar-month-wrap" class="hidden px-3 pb-2.5">
|
||||
<div class="grid grid-cols-7 gap-0.5 text-center text-[9px] font-medium text-gray-400 uppercase tracking-wide mb-0.5 leading-none">
|
||||
${WEEKDAYS_SHORT.map((d) => `<div>${d}</div>`).join('')}
|
||||
</div>
|
||||
<div id="calendar-month-grid" class="grid grid-cols-7 gap-0.5"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex-1 overflow-y-auto px-4 pt-3">
|
||||
<p class="text-sm text-gray-500 leading-relaxed">Dotknij dnia w kalendarzu, aby później przypisać posiłki. Ta sekcja na razie jest zapowiedzią rozbudowy planera.</p>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
function renderWeekGrid(weekStart, selected) {
|
||||
const grid = document.getElementById('calendar-week-grid');
|
||||
if (!grid) return;
|
||||
|
||||
const cells = [];
|
||||
for (let i = 0; i < 7; i++) {
|
||||
const day = addDays(weekStart, i);
|
||||
const isSel = selected && sameDay(day, selected);
|
||||
const isToday = sameDay(day, new Date());
|
||||
cells.push(`
|
||||
<button type="button" data-planner-day="${day.getTime()}"
|
||||
class="aspect-square flex flex-col items-center justify-center rounded-md text-xs font-medium transition-colors min-h-0
|
||||
${isSel ? 'bg-gray-900 text-white' : 'text-gray-800 hover:bg-gray-100'}
|
||||
${isToday && !isSel ? 'ring-1 ring-inset ring-gray-900' : ''}">
|
||||
<span>${day.getDate()}</span>
|
||||
</button>
|
||||
`);
|
||||
}
|
||||
grid.innerHTML = cells.join('');
|
||||
}
|
||||
|
||||
function renderMonthGrid(monthAnchor, selected) {
|
||||
const grid = document.getElementById('calendar-month-grid');
|
||||
if (!grid) return;
|
||||
|
||||
const first = startOfMonth(monthAnchor);
|
||||
const startGrid = startOfWeekMonday(first);
|
||||
const cells = [];
|
||||
for (let i = 0; i < 42; i++) {
|
||||
const day = addDays(startGrid, i);
|
||||
const inMonth = day.getMonth() === first.getMonth();
|
||||
const isSel = selected && sameDay(day, selected);
|
||||
const isToday = sameDay(day, new Date());
|
||||
cells.push(`
|
||||
<button type="button" data-planner-day="${day.getTime()}"
|
||||
class="aspect-square flex flex-col items-center justify-center rounded-md text-xs font-medium transition-colors min-h-0
|
||||
${!inMonth ? 'text-gray-300' : (isSel ? 'bg-gray-900 text-white' : 'text-gray-800 hover:bg-gray-100')}
|
||||
${inMonth && isToday && !isSel ? 'ring-1 ring-inset ring-gray-900' : ''}">
|
||||
<span>${day.getDate()}</span>
|
||||
</button>
|
||||
`);
|
||||
}
|
||||
grid.innerHTML = cells.join('');
|
||||
}
|
||||
|
||||
function updatePeriodLabel(mode, weekStart, monthAnchor) {
|
||||
const el = document.getElementById('cal-period-label');
|
||||
if (!el) return;
|
||||
|
||||
if (mode === 'week') {
|
||||
const end = addDays(weekStart, 6);
|
||||
const y = weekStart.getFullYear();
|
||||
if (weekStart.getMonth() === end.getMonth()) {
|
||||
el.textContent = `${weekStart.getDate()}–${end.getDate()} ${MONTHS_SHORT[weekStart.getMonth()]} ${y}`;
|
||||
} else {
|
||||
el.textContent = `${weekStart.getDate()} ${MONTHS_SHORT[weekStart.getMonth()]} – ${end.getDate()} ${MONTHS_SHORT[end.getMonth()]} ${y}`;
|
||||
}
|
||||
} else {
|
||||
const m = monthAnchor.getMonth();
|
||||
const y = monthAnchor.getFullYear();
|
||||
const monthLong = [
|
||||
'Styczeń', 'Luty', 'Marzec', 'Kwiecień', 'Maj', 'Czerwiec',
|
||||
'Lipiec', 'Sierpień', 'Wrzesień', 'Październik', 'Listopad', 'Grudzień',
|
||||
][m];
|
||||
el.textContent = `${monthLong} ${y}`;
|
||||
}
|
||||
}
|
||||
|
||||
function syncModeToggle(mode) {
|
||||
const w = document.getElementById('planner-mode-week');
|
||||
const m = document.getElementById('planner-mode-month');
|
||||
const weekWrap = document.getElementById('calendar-week-wrap');
|
||||
const monthWrap = document.getElementById('calendar-month-wrap');
|
||||
|
||||
const base = 'planner-cal-mode-btn w-7 h-6 flex items-center justify-center rounded-[0.3125rem] transition-colors';
|
||||
const active = `${base} bg-white text-gray-900 shadow-sm`;
|
||||
const idle = `${base} text-gray-400 hover:text-gray-600`;
|
||||
|
||||
if (w && m) {
|
||||
if (mode === 'week') {
|
||||
w.className = active;
|
||||
m.className = idle;
|
||||
} else {
|
||||
w.className = idle;
|
||||
m.className = active;
|
||||
}
|
||||
}
|
||||
if (weekWrap && monthWrap) {
|
||||
weekWrap.classList.toggle('hidden', mode !== 'week');
|
||||
monthWrap.classList.toggle('hidden', mode !== 'month');
|
||||
}
|
||||
}
|
||||
|
||||
function bindDayClicks(container, state, rerender) {
|
||||
container?.addEventListener('click', (e) => {
|
||||
const btn = e.target.closest('[data-planner-day]');
|
||||
if (!btn) return;
|
||||
const ts = Number(btn.getAttribute('data-planner-day'));
|
||||
state.selected = new Date(ts);
|
||||
rerender();
|
||||
});
|
||||
}
|
||||
|
||||
export function setupMealPlanner() {
|
||||
const state = {
|
||||
mode: 'week',
|
||||
weekStart: startOfWeekMonday(new Date()),
|
||||
monthAnchor: startOfDay(new Date()),
|
||||
selected: startOfDay(new Date()),
|
||||
};
|
||||
|
||||
const weekGrid = document.getElementById('calendar-week-grid');
|
||||
const monthGrid = document.getElementById('calendar-month-grid');
|
||||
|
||||
const rerender = () => {
|
||||
syncModeToggle(state.mode);
|
||||
updatePeriodLabel(state.mode, state.weekStart, state.monthAnchor);
|
||||
syncTodayButton(state.mode, state.weekStart, state.monthAnchor, state.selected);
|
||||
if (state.mode === 'week') {
|
||||
renderWeekGrid(state.weekStart, state.selected);
|
||||
} else {
|
||||
renderMonthGrid(state.monthAnchor, state.selected);
|
||||
}
|
||||
};
|
||||
|
||||
bindDayClicks(weekGrid?.parentElement, state, rerender);
|
||||
bindDayClicks(monthGrid?.parentElement, state, rerender);
|
||||
|
||||
document.getElementById('planner-cal-mode')?.addEventListener('click', (e) => {
|
||||
const btn = e.target.closest('[data-cal-mode]');
|
||||
if (!btn) return;
|
||||
const mode = btn.getAttribute('data-cal-mode');
|
||||
if (mode !== 'week' && mode !== 'month') return;
|
||||
state.mode = mode;
|
||||
if (mode === 'week') {
|
||||
state.weekStart = startOfWeekMonday(state.selected);
|
||||
} else {
|
||||
state.monthAnchor = startOfMonth(state.selected);
|
||||
}
|
||||
rerender();
|
||||
});
|
||||
|
||||
document.getElementById('cal-prev')?.addEventListener('click', () => {
|
||||
if (state.mode === 'week') {
|
||||
state.weekStart = addWeeks(state.weekStart, -1);
|
||||
if (!weekContains(state.weekStart, state.selected)) {
|
||||
state.selected = new Date(state.weekStart);
|
||||
}
|
||||
} else {
|
||||
state.monthAnchor = addMonths(state.monthAnchor, -1);
|
||||
if (!sameMonth(state.monthAnchor, state.selected)) {
|
||||
state.selected = startOfMonth(state.monthAnchor);
|
||||
}
|
||||
}
|
||||
rerender();
|
||||
});
|
||||
|
||||
document.getElementById('cal-next')?.addEventListener('click', () => {
|
||||
if (state.mode === 'week') {
|
||||
state.weekStart = addWeeks(state.weekStart, 1);
|
||||
if (!weekContains(state.weekStart, state.selected)) {
|
||||
state.selected = new Date(state.weekStart);
|
||||
}
|
||||
} else {
|
||||
state.monthAnchor = addMonths(state.monthAnchor, 1);
|
||||
if (!sameMonth(state.monthAnchor, state.selected)) {
|
||||
state.selected = startOfMonth(state.monthAnchor);
|
||||
}
|
||||
}
|
||||
rerender();
|
||||
});
|
||||
|
||||
document.getElementById('cal-go-today')?.addEventListener('click', () => {
|
||||
const today = startOfDay(new Date());
|
||||
state.selected = today;
|
||||
state.weekStart = startOfWeekMonday(today);
|
||||
state.monthAnchor = startOfMonth(today);
|
||||
rerender();
|
||||
});
|
||||
|
||||
rerender();
|
||||
}
|
||||
292
stacks/recipe/js/views/RecipeDetail.js
Normal file
292
stacks/recipe/js/views/RecipeDetail.js
Normal file
@@ -0,0 +1,292 @@
|
||||
export function getRecipeDetailHTML() {
|
||||
return `
|
||||
<div id="recipe-detail-view" class="absolute inset-0 bg-white z-30 transition-all duration-300 ease-in-out translate-x-full opacity-0 pointer-events-none flex flex-col overflow-hidden">
|
||||
|
||||
<div class="absolute top-0 w-full p-4 flex justify-between z-40 mt-4">
|
||||
<button onclick="closeRecipeDetail()" class="w-10 h-10 bg-white/90 backdrop-blur rounded-full flex items-center justify-center shadow-sm text-gray-800 hover:bg-white transition-colors">
|
||||
<i class="fas fa-arrow-left"></i>
|
||||
</button>
|
||||
<button class="w-10 h-10 bg-white/90 backdrop-blur rounded-full flex items-center justify-center shadow-sm text-gray-400 hover:text-red-500 transition-colors">
|
||||
<i class="far fa-heart"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="h-[260px] shrink-0 w-full bg-[#d4d4d4] flex items-center justify-center relative">
|
||||
<span class="text-white font-medium text-lg">Zdjęcie: Serek z owocami</span>
|
||||
</div>
|
||||
|
||||
<div class="bg-white rounded-t-3xl -mt-6 relative z-30 pt-8 flex flex-col flex-1 overflow-hidden">
|
||||
|
||||
<div class="mb-4 px-6 shrink-0">
|
||||
<div class="flex justify-between items-start mb-3">
|
||||
<h1 class="text-2xl font-bold text-gray-900">Serek wiejski z orzechami i owocami</h1>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-wrap gap-2 mb-4">
|
||||
<span class="px-3 py-1 bg-gray-100 text-gray-700 text-xs rounded-md font-medium">Śniadanie</span>
|
||||
<span class="px-3 py-1 bg-gray-100 text-gray-700 text-xs rounded-md font-medium">Wegetariańskie</span>
|
||||
<span class="px-3 py-1 bg-gray-100 text-gray-700 text-xs rounded-md font-medium">Słodkie</span>
|
||||
</div>
|
||||
|
||||
<div class="flex justify-between items-center text-sm text-gray-600 font-medium">
|
||||
<div class="flex gap-4">
|
||||
<div class="flex items-center gap-1.5"><i class="fas fa-clock text-gray-400"></i><span>5 min</span></div>
|
||||
<div class="flex items-center gap-1.5"><i class="fas fa-fire text-gray-400"></i><span>642 kcal</span></div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-1 bg-gray-100 p-1 rounded-lg">
|
||||
<button onclick="changeServings(-1)" class="w-6 h-6 bg-white rounded shadow-sm flex items-center justify-center text-gray-600 hover:text-black hover:bg-gray-50"><i class="fas fa-minus text-[10px]"></i></button>
|
||||
<div class="flex items-center gap-1 px-2">
|
||||
<span id="servings-count" class="font-bold text-gray-900 text-sm w-3 text-center">1</span>
|
||||
<span class="text-xs text-gray-500"><i class="fas fa-user-friends"></i></span>
|
||||
</div>
|
||||
<button onclick="changeServings(1)" class="w-6 h-6 bg-white rounded shadow-sm flex items-center justify-center text-gray-600 hover:text-black hover:bg-gray-50"><i class="fas fa-plus text-[10px]"></i></button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex border-b border-gray-200 mb-2 px-6 shrink-0">
|
||||
<button class="flex-1 pb-3 text-sm font-semibold text-gray-900 border-b-2 border-gray-900 tab-btn" onclick="switchTab('ingredients', this)">Składniki</button>
|
||||
<button class="flex-1 pb-3 text-sm font-medium text-gray-500 border-b-2 border-transparent hover:text-gray-700 tab-btn" onclick="switchTab('steps', this)">Kroki</button>
|
||||
<button class="flex-1 pb-3 text-sm font-medium text-gray-500 border-b-2 border-transparent hover:text-gray-700 tab-btn" onclick="switchTab('nutrition', this)">Wartości</button>
|
||||
</div>
|
||||
|
||||
<div class="flex-1 overflow-y-auto px-6 pt-2 pb-10 no-scrollbar relative">
|
||||
|
||||
<div id="tab-ingredients" class="tab-content block animate-fade-in">
|
||||
<div class="flex justify-between items-end mb-4">
|
||||
<span class="text-xs text-gray-500 font-medium">Zaznacz, by dodać do listy zakupów</span>
|
||||
</div>
|
||||
|
||||
<ul class="space-y-0 mb-6" id="ingredient-list">
|
||||
<li class="flex items-center gap-3 py-3 border-b border-gray-100 cursor-pointer hover:bg-gray-50 px-1 -mx-1 transition-colors" onclick="toggleIngredient(this)">
|
||||
<div class="w-5 h-5 rounded border border-gray-300 flex items-center justify-center text-white check-box transition-colors"><i class="fas fa-check text-[10px] hidden check-icon"></i></div>
|
||||
<span class="text-gray-700 text-sm flex-1 ingredient-text transition-colors">Serek wiejski</span>
|
||||
<span class="font-medium text-gray-900 text-sm ingredient-amount" data-base-amount="200" data-unit="g">200 g</span>
|
||||
</li>
|
||||
|
||||
<li class="flex items-center gap-3 py-3 border-b border-gray-100 cursor-pointer hover:bg-gray-50 px-1 -mx-1 transition-colors" onclick="toggleIngredient(this)">
|
||||
<div class="w-5 h-5 rounded border border-gray-300 flex items-center justify-center text-white check-box transition-colors"><i class="fas fa-check text-[10px] hidden check-icon"></i></div>
|
||||
<span class="text-gray-700 text-sm flex-1 ingredient-text transition-colors">Miód</span>
|
||||
<span class="font-medium text-gray-900 text-sm ingredient-amount" data-base-amount="10" data-unit="g">10 g</span>
|
||||
</li>
|
||||
|
||||
<li class="flex items-center gap-3 py-3 border-b border-gray-100 cursor-pointer hover:bg-gray-50 px-1 -mx-1 transition-colors" onclick="toggleIngredient(this)">
|
||||
<div class="w-5 h-5 rounded border border-gray-300 flex items-center justify-center text-white check-box transition-colors"><i class="fas fa-check text-[10px] hidden check-icon"></i></div>
|
||||
<span class="text-gray-700 text-sm flex-1 ingredient-text transition-colors font-medium text-gray-900" id="ingredient-orzechy">Orzechy włoskie</span>
|
||||
<div class="flex items-center gap-3">
|
||||
<button onclick="event.stopPropagation(); openSwapModal('orzechy')" class="w-7 h-7 flex items-center justify-center rounded-full bg-gray-100 text-gray-600 hover:bg-gray-200 hover:text-gray-900 transition-colors shadow-sm" title="Zamień">
|
||||
<i class="fas fa-exchange-alt text-[10px]"></i>
|
||||
</button>
|
||||
<span class="font-medium text-gray-900 text-sm ingredient-amount w-10 text-right" data-base-amount="50" data-unit="g">50 g</span>
|
||||
</div>
|
||||
</li>
|
||||
|
||||
<li class="flex items-center gap-3 py-3 border-b border-gray-100 cursor-pointer hover:bg-gray-50 px-1 -mx-1 transition-colors" onclick="toggleIngredient(this)">
|
||||
<div class="w-5 h-5 rounded border border-gray-300 flex items-center justify-center text-white check-box transition-colors"><i class="fas fa-check text-[10px] hidden check-icon"></i></div>
|
||||
<span class="text-gray-700 text-sm flex-1 ingredient-text transition-colors font-medium text-gray-900" id="ingredient-owoce1">Truskawki</span>
|
||||
<div class="flex items-center gap-3">
|
||||
<button onclick="event.stopPropagation(); openSwapModal('owoce1')" class="w-7 h-7 flex items-center justify-center rounded-full bg-gray-100 text-gray-600 hover:bg-gray-200 hover:text-gray-900 transition-colors shadow-sm" title="Zamień">
|
||||
<i class="fas fa-exchange-alt text-[10px]"></i>
|
||||
</button>
|
||||
<span class="font-medium text-gray-900 text-sm ingredient-amount w-10 text-right" data-base-amount="100" data-unit="g">100 g</span>
|
||||
</div>
|
||||
</li>
|
||||
|
||||
<li class="flex items-center gap-3 py-3 border-b border-gray-100 cursor-pointer hover:bg-gray-50 px-1 -mx-1 transition-colors" onclick="toggleIngredient(this)">
|
||||
<div class="w-5 h-5 rounded border border-gray-300 flex items-center justify-center text-white check-box transition-colors"><i class="fas fa-check text-[10px] hidden check-icon"></i></div>
|
||||
<span class="text-gray-700 text-sm flex-1 ingredient-text transition-colors font-medium text-gray-900" id="ingredient-owoce2">Borówki ameryk.</span>
|
||||
<div class="flex items-center gap-3">
|
||||
<button onclick="event.stopPropagation(); openSwapModal('owoce2')" class="w-7 h-7 flex items-center justify-center rounded-full bg-gray-100 text-gray-600 hover:bg-gray-200 hover:text-gray-900 transition-colors shadow-sm" title="Zamień">
|
||||
<i class="fas fa-exchange-alt text-[10px]"></i>
|
||||
</button>
|
||||
<span class="font-medium text-gray-900 text-sm ingredient-amount w-10 text-right" data-base-amount="100" data-unit="g">100 g</span>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<button class="w-full bg-gray-900 hover:bg-black text-white py-3.5 rounded-xl font-semibold shadow-sm transition-colors text-sm flex items-center justify-center gap-2 mb-6">
|
||||
<i class="fas fa-plus"></i> Dodaj do listy zakupów
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div id="tab-steps" class="tab-content hidden animate-fade-in">
|
||||
<div class="space-y-6 pb-6">
|
||||
<div class="flex gap-4">
|
||||
<div class="w-7 h-7 rounded-full bg-gray-900 text-white flex items-center justify-center text-sm font-bold shrink-0 shadow-sm">1</div>
|
||||
<div class="pt-0.5"><p class="text-sm text-gray-600 leading-relaxed">Przełóż serek wiejski do miseczki.</p></div>
|
||||
</div>
|
||||
<div class="flex gap-4">
|
||||
<div class="w-7 h-7 rounded-full bg-gray-900 text-white flex items-center justify-center text-sm font-bold shrink-0 shadow-sm">2</div>
|
||||
<div class="pt-0.5"><p class="text-sm text-gray-600 leading-relaxed">Dodaj miód i delikatnie wymieszaj.</p></div>
|
||||
</div>
|
||||
<div class="flex gap-4">
|
||||
<div class="w-7 h-7 rounded-full bg-gray-900 text-white flex items-center justify-center text-sm font-bold shrink-0 shadow-sm">3</div>
|
||||
<div class="pt-0.5"><p class="text-sm text-gray-600 leading-relaxed">Orzechy posiekaj na mniejsze kawałki i posyp nimi serek z miodem.</p></div>
|
||||
</div>
|
||||
<div class="flex gap-4">
|
||||
<div class="w-7 h-7 rounded-full bg-gray-900 text-white flex items-center justify-center text-sm font-bold shrink-0 shadow-sm">4</div>
|
||||
<div class="pt-0.5"><p class="text-sm text-gray-600 leading-relaxed">Umyj owoce (ew. pokrój na połówki) i ułóż na wierzchu. Gotowe!</p></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="tab-nutrition" class="tab-content hidden animate-fade-in">
|
||||
<div class="bg-gray-50 rounded-xl p-5 border border-gray-100 mb-6">
|
||||
<h3 class="font-bold text-gray-900 border-b border-gray-200 pb-2 mb-2 text-lg">Wartości odżywcze</h3>
|
||||
<p class="text-xs text-gray-500 mb-4">Dla bazowej porcji (1 porcja)</p>
|
||||
<ul class="space-y-0 divide-y divide-gray-200">
|
||||
<li class="flex justify-between py-2.5 font-bold"><span class="text-gray-900 text-sm">Kalorie</span><span class="text-gray-900 text-sm">642 kcal</span></li>
|
||||
<li class="flex justify-between py-2.5"><span class="text-gray-800 text-sm font-medium">Białko</span><span class="font-medium text-gray-900 text-sm">32 g</span></li>
|
||||
<li class="flex justify-between py-2.5"><span class="text-gray-800 text-sm font-medium">Tłuszcze</span><span class="font-medium text-gray-900 text-sm">43 g</span></li>
|
||||
<li class="flex justify-between py-2.5"><span class="text-gray-800 text-sm font-medium">Węglowodany</span><span class="font-medium text-gray-900 text-sm">41 g</span></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="swap-backdrop" onclick="closeSwapModal()" class="absolute inset-0 bg-black/40 z-40 hidden opacity-0 transition-opacity duration-300"></div>
|
||||
|
||||
<div id="swap-modal" class="absolute inset-x-0 bottom-0 bg-white rounded-t-3xl shadow-[0_-10px_40px_rgba(0,0,0,0.1)] z-50 transform translate-y-full transition-transform duration-300 ease-in-out p-6 flex flex-col max-h-[60%]">
|
||||
<div class="flex justify-between items-center mb-5 shrink-0">
|
||||
<h3 class="text-xl font-bold text-gray-900">Zmień <span id="swap-title-target" class="text-blue-600">składnik</span></h3>
|
||||
<button onclick="closeSwapModal()" class="w-8 h-8 flex items-center justify-center bg-gray-100 rounded-full text-gray-500 hover:bg-gray-200 hover:text-gray-900 transition-colors">
|
||||
<i class="fas fa-times text-sm"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div id="swap-options-container" class="space-y-2 overflow-y-auto no-scrollbar pb-2">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
export function setupRecipeDetail() {
|
||||
let currentServings = 1; // Domślnie 1 porcja dla tego przepisu
|
||||
const defaultServings = 1;
|
||||
let currentlySwapping = null;
|
||||
|
||||
// Dane do dynamicznego modala
|
||||
const swapOptions = {
|
||||
'orzechy': [
|
||||
{ name: 'Orzechy włoskie', hint: 'Bazowe', color: 'gray' },
|
||||
{ name: 'Migdały', hint: '+ Białko', color: 'blue' },
|
||||
{ name: 'Orzechy laskowe', hint: 'Klasyk', color: 'gray' },
|
||||
{ name: 'Orzechy nerkowca', hint: 'Słodsze', color: 'gray' },
|
||||
{ name: 'Orzechy pekan', hint: '+ Tłuszcz', color: 'green' }
|
||||
],
|
||||
'owoce1': [
|
||||
{ name: 'Truskawki', hint: 'Bazowe', color: 'gray' },
|
||||
{ name: 'Gruszka konferencja', hint: '+ Węgle', color: 'blue' },
|
||||
{ name: 'Banany', hint: '+ Kalorie', color: 'green' }
|
||||
],
|
||||
'owoce2': [
|
||||
{ name: 'Borówki ameryk.', hint: 'Bazowe', color: 'gray' },
|
||||
{ name: 'Jagody leśne', hint: 'Sezonowe', color: 'blue' },
|
||||
{ name: 'Maliny', hint: '- Kalorie', color: 'green' }
|
||||
]
|
||||
};
|
||||
|
||||
window.switchTab = (tabId, clickedBtn) => {
|
||||
document.querySelectorAll('.tab-content').forEach(el => {
|
||||
el.classList.remove('block');
|
||||
el.classList.add('hidden');
|
||||
});
|
||||
|
||||
const targetTab = document.getElementById(`tab-${tabId}`);
|
||||
targetTab.classList.remove('hidden');
|
||||
targetTab.classList.add('block');
|
||||
targetTab.parentElement.scrollTop = 0;
|
||||
|
||||
document.querySelectorAll('.tab-btn').forEach(btn => {
|
||||
btn.classList.remove('text-gray-900', 'border-gray-900', 'font-semibold');
|
||||
btn.classList.add('text-gray-500', 'border-transparent', 'font-medium');
|
||||
});
|
||||
|
||||
clickedBtn.classList.remove('text-gray-500', 'border-transparent', 'font-medium');
|
||||
clickedBtn.classList.add('text-gray-900', 'border-gray-900', 'font-semibold');
|
||||
};
|
||||
|
||||
window.toggleIngredient = (element) => {
|
||||
element.classList.toggle('ingredient-active');
|
||||
};
|
||||
|
||||
window.changeServings = (delta) => {
|
||||
const newServings = currentServings + delta;
|
||||
if (newServings < 1) return;
|
||||
|
||||
currentServings = newServings;
|
||||
document.getElementById('servings-count').innerText = currentServings;
|
||||
|
||||
const ratio = currentServings / defaultServings;
|
||||
document.querySelectorAll('.ingredient-amount').forEach(el => {
|
||||
const baseAmount = parseFloat(el.getAttribute('data-base-amount'));
|
||||
const unit = el.getAttribute('data-unit');
|
||||
|
||||
if (!isNaN(baseAmount)) {
|
||||
let newAmount = baseAmount * ratio;
|
||||
newAmount = Number.isInteger(newAmount) ? newAmount : parseFloat(newAmount.toFixed(1));
|
||||
el.innerText = `${newAmount} ${unit}`;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
window.openSwapModal = (type) => {
|
||||
currentlySwapping = type;
|
||||
|
||||
let title = '';
|
||||
if(type === 'orzechy') title = 'Orzechy';
|
||||
if(type === 'owoce1') title = 'Owoce bazy';
|
||||
if(type === 'owoce2') title = 'Dodatki owocowe';
|
||||
|
||||
document.getElementById('swap-title-target').innerText = title;
|
||||
|
||||
// Wygeneruj opcje na podstawie słownika
|
||||
const container = document.getElementById('swap-options-container');
|
||||
container.innerHTML = swapOptions[type].map(opt => {
|
||||
let badgeClass = 'text-gray-600 bg-gray-200'; // Domyślny gray
|
||||
if (opt.color === 'blue') badgeClass = 'text-blue-600 bg-blue-100';
|
||||
if (opt.color === 'green') badgeClass = 'text-green-600 bg-green-100';
|
||||
|
||||
return `
|
||||
<button onclick="confirmSwap('${opt.name}')" class="w-full flex justify-between items-center p-4 border border-gray-200 rounded-xl hover:border-gray-900 hover:shadow-sm transition-all bg-gray-50 hover:bg-white text-left">
|
||||
<span class="font-medium text-gray-800">${opt.name}</span>
|
||||
<span class="text-[11px] px-2 py-1 rounded-md font-semibold ${badgeClass}">${opt.hint}</span>
|
||||
</button>
|
||||
`;
|
||||
}).join('');
|
||||
|
||||
const backdrop = document.getElementById('swap-backdrop');
|
||||
backdrop.classList.remove('hidden');
|
||||
setTimeout(() => backdrop.classList.remove('opacity-0'), 10);
|
||||
|
||||
const modal = document.getElementById('swap-modal');
|
||||
modal.classList.remove('translate-y-full');
|
||||
modal.classList.add('translate-y-0');
|
||||
};
|
||||
|
||||
window.closeSwapModal = () => {
|
||||
const backdrop = document.getElementById('swap-backdrop');
|
||||
backdrop.classList.add('opacity-0');
|
||||
setTimeout(() => backdrop.classList.add('hidden'), 300);
|
||||
|
||||
const modal = document.getElementById('swap-modal');
|
||||
modal.classList.remove('translate-y-0');
|
||||
modal.classList.add('translate-y-full');
|
||||
};
|
||||
|
||||
window.confirmSwap = (newItemName) => {
|
||||
if (currentlySwapping === 'orzechy') {
|
||||
document.getElementById('ingredient-orzechy').innerText = newItemName;
|
||||
} else if (currentlySwapping === 'owoce1') {
|
||||
document.getElementById('ingredient-owoce1').innerText = newItemName;
|
||||
} else if (currentlySwapping === 'owoce2') {
|
||||
document.getElementById('ingredient-owoce2').innerText = newItemName;
|
||||
}
|
||||
closeSwapModal();
|
||||
};
|
||||
}
|
||||
174
stacks/recipe/js/views/RecipeList.js
Normal file
174
stacks/recipe/js/views/RecipeList.js
Normal file
@@ -0,0 +1,174 @@
|
||||
export function getRecipeListHTML() {
|
||||
return `
|
||||
<div id="main-view" class="flex flex-col h-full absolute inset-0 bg-gray-50 z-10">
|
||||
<div class="p-4 border-b border-gray-200 mt-4 bg-white">
|
||||
<div class="flex items-center w-full border border-gray-300 rounded-lg bg-white focus-within:border-gray-400 transition-colors">
|
||||
<div class="pl-3 pr-2 text-gray-400"><i class="fas fa-search"></i></div>
|
||||
<input type="text" placeholder="Szukaj przepisów..." class="flex-1 py-2.5 bg-transparent outline-none text-gray-600 placeholder-gray-400 text-sm">
|
||||
<div class="w-px h-6 bg-gray-200"></div>
|
||||
<button onclick="openFilters()" class="px-4 text-gray-700 hover:text-black flex items-center justify-center transition-colors">
|
||||
<i class="fas fa-sliders-h"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex-1 overflow-y-auto px-4 pt-4 pb-24 bg-gray-50">
|
||||
<div class="grid grid-cols-2 gap-3">
|
||||
|
||||
<div onclick="openRecipeDetail()" class="border border-gray-200 rounded-xl overflow-hidden shadow-sm flex flex-col bg-white cursor-pointer hover:shadow-md transition-shadow">
|
||||
<div class="h-32 bg-[#d4d4d4] relative flex items-center justify-center">
|
||||
<span class="text-white font-medium text-xs">Placki</span>
|
||||
</div>
|
||||
<div class="p-3 flex flex-col flex-1">
|
||||
<h3 class="text-sm font-medium underline decoration-1 underline-offset-2 text-black mb-1 line-clamp-1">Puszyste placki</h3>
|
||||
<p class="text-gray-500 text-xs mb-3 line-clamp-2">Klasyczne placki na śniadanie</p>
|
||||
<div class="mt-auto">
|
||||
<div class="flex items-center justify-between text-[11px] text-gray-600 font-medium mb-2">
|
||||
<div class="flex items-center gap-1"><i class="fas fa-clock text-gray-400"></i><span>15 min</span></div>
|
||||
<div class="flex items-center gap-1"><i class="fas fa-fire text-gray-400"></i><span>320 kcal</span></div>
|
||||
</div>
|
||||
<div class="flex flex-wrap gap-1">
|
||||
<span class="px-2 py-0.5 bg-gray-100 text-gray-600 text-[10px] rounded-md font-medium">Śniadanie</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div onclick="openRecipeDetail()" class="border border-gray-200 rounded-xl overflow-hidden shadow-sm flex flex-col bg-white cursor-pointer hover:shadow-md transition-shadow">
|
||||
<div class="h-32 bg-[#d4d4d4] relative flex items-center justify-center">
|
||||
<span class="text-white font-medium text-xs">Sałatka</span>
|
||||
</div>
|
||||
<div class="p-3 flex flex-col flex-1">
|
||||
<h3 class="text-sm font-medium underline decoration-1 underline-offset-2 text-black mb-1 line-clamp-1">Sałatka z kurczakiem</h3>
|
||||
<p class="text-gray-500 text-xs mb-3 line-clamp-2">Zielone warzywa z grillowanym kurczakiem</p>
|
||||
<div class="mt-auto">
|
||||
<div class="flex items-center justify-between text-[11px] text-gray-600 font-medium mb-2">
|
||||
<div class="flex items-center gap-1"><i class="fas fa-clock text-gray-400"></i><span>20 min</span></div>
|
||||
<div class="flex items-center gap-1"><i class="fas fa-fire text-gray-400"></i><span>250 kcal</span></div>
|
||||
</div>
|
||||
<div class="flex flex-wrap gap-1">
|
||||
<span class="px-2 py-0.5 bg-gray-100 text-gray-600 text-[10px] rounded-md font-medium">Obiad</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div onclick="openRecipeDetail()" class="border border-gray-200 rounded-xl overflow-hidden shadow-sm flex flex-col bg-white cursor-pointer hover:shadow-md transition-shadow">
|
||||
<div class="h-32 bg-[#d4d4d4] relative flex items-center justify-center">
|
||||
<span class="text-white font-medium text-xs">Makaron</span>
|
||||
</div>
|
||||
<div class="p-3 flex flex-col flex-1">
|
||||
<h3 class="text-sm font-medium underline decoration-1 underline-offset-2 text-black mb-1 line-clamp-1">Makaron z pomidorami i bazylią</h3>
|
||||
<p class="text-gray-500 text-xs mb-3 line-clamp-2">Aromatyczny sos pomidorowy z czosnkiem</p>
|
||||
<div class="mt-auto">
|
||||
<div class="flex items-center justify-between text-[11px] text-gray-600 font-medium mb-2">
|
||||
<div class="flex items-center gap-1"><i class="fas fa-clock text-gray-400"></i><span>30 min</span></div>
|
||||
<div class="flex items-center gap-1"><i class="fas fa-fire text-gray-400"></i><span>450 kcal</span></div>
|
||||
</div>
|
||||
<div class="flex flex-wrap gap-1">
|
||||
<span class="px-2 py-0.5 bg-gray-100 text-gray-600 text-[10px] rounded-md font-medium">Kolacja</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div onclick="openRecipeDetail()" class="border border-gray-200 rounded-xl overflow-hidden shadow-sm flex flex-col bg-white cursor-pointer hover:shadow-md transition-shadow">
|
||||
<div class="h-32 bg-[#d4d4d4] relative flex items-center justify-center">
|
||||
<span class="text-white font-medium text-xs">Koktajl</span>
|
||||
</div>
|
||||
<div class="p-3 flex flex-col flex-1">
|
||||
<h3 class="text-sm font-medium underline decoration-1 underline-offset-2 text-black mb-1 line-clamp-1">Koktajl owocowy</h3>
|
||||
<p class="text-gray-500 text-xs mb-3 line-clamp-2">Mix jagód i jogurtu</p>
|
||||
<div class="mt-auto">
|
||||
<div class="flex items-center justify-between text-[11px] text-gray-600 font-medium mb-2">
|
||||
<div class="flex items-center gap-1"><i class="fas fa-clock text-gray-400"></i><span>5 min</span></div>
|
||||
<div class="flex items-center gap-1"><i class="fas fa-fire text-gray-400"></i><span>180 kcal</span></div>
|
||||
</div>
|
||||
<div class="flex flex-wrap gap-1">
|
||||
<span class="px-2 py-0.5 bg-gray-100 text-gray-600 text-[10px] rounded-md font-medium">Przekąska</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div onclick="openRecipeDetail()" class="border border-gray-200 rounded-xl overflow-hidden shadow-sm flex flex-col bg-white cursor-pointer hover:shadow-md transition-shadow">
|
||||
<div class="h-32 bg-[#d4d4d4] relative flex items-center justify-center">
|
||||
<span class="text-white font-medium text-xs">Tost z awokado</span>
|
||||
</div>
|
||||
<div class="p-3 flex flex-col flex-1">
|
||||
<h3 class="text-sm font-medium underline decoration-1 underline-offset-2 text-black mb-1 line-clamp-1">Tost z awokado</h3>
|
||||
<p class="text-gray-500 text-xs mb-3 line-clamp-2">Chleb na zakwasie z rozgniecionym awokado</p>
|
||||
<div class="mt-auto">
|
||||
<div class="flex items-center justify-between text-[11px] text-gray-600 font-medium mb-2">
|
||||
<div class="flex items-center gap-1"><i class="fas fa-clock text-gray-400"></i><span>10 min</span></div>
|
||||
<div class="flex items-center gap-1"><i class="fas fa-fire text-gray-400"></i><span>220 kcal</span></div>
|
||||
</div>
|
||||
<div class="flex flex-wrap gap-1">
|
||||
<span class="px-2 py-0.5 bg-gray-100 text-gray-600 text-[10px] rounded-md font-medium">Śniadanie</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div onclick="openRecipeDetail()" class="border border-gray-200 rounded-xl overflow-hidden shadow-sm flex flex-col bg-white cursor-pointer hover:shadow-md transition-shadow">
|
||||
<div class="h-32 bg-[#d4d4d4] relative flex items-center justify-center">
|
||||
<span class="text-white font-medium text-xs">Łosoś</span>
|
||||
</div>
|
||||
<div class="p-3 flex flex-col flex-1">
|
||||
<h3 class="text-sm font-medium underline decoration-1 underline-offset-2 text-black mb-1 line-clamp-1">Grillowany łosoś</h3>
|
||||
<p class="text-gray-500 text-xs mb-3 line-clamp-2">Świeży łosoś z masłem cytrynowym</p>
|
||||
<div class="mt-auto">
|
||||
<div class="flex items-center justify-between text-[11px] text-gray-600 font-medium mb-2">
|
||||
<div class="flex items-center gap-1"><i class="fas fa-clock text-gray-400"></i><span>25 min</span></div>
|
||||
<div class="flex items-center gap-1"><i class="fas fa-fire text-gray-400"></i><span>380 kcal</span></div>
|
||||
</div>
|
||||
<div class="flex flex-wrap gap-1">
|
||||
<span class="px-2 py-0.5 bg-gray-100 text-gray-600 text-[10px] rounded-md font-medium">Kolacja</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div onclick="openRecipeDetail()" class="border border-gray-200 rounded-xl overflow-hidden shadow-sm flex flex-col bg-white cursor-pointer hover:shadow-md transition-shadow">
|
||||
<div class="h-32 bg-[#d4d4d4] relative flex items-center justify-center">
|
||||
<span class="text-white font-medium text-xs">Tacos</span>
|
||||
</div>
|
||||
<div class="p-3 flex flex-col flex-1">
|
||||
<h3 class="text-sm font-medium underline decoration-1 underline-offset-2 text-black mb-1 line-clamp-1">Tacos z wołowiną</h3>
|
||||
<p class="text-gray-500 text-xs mb-3 line-clamp-2">Pikantna mielona wołowina ze świeżą salsą</p>
|
||||
<div class="mt-auto">
|
||||
<div class="flex items-center justify-between text-[11px] text-gray-600 font-medium mb-2">
|
||||
<div class="flex items-center gap-1"><i class="fas fa-clock text-gray-400"></i><span>20 min</span></div>
|
||||
<div class="flex items-center gap-1"><i class="fas fa-fire text-gray-400"></i><span>410 kcal</span></div>
|
||||
</div>
|
||||
<div class="flex flex-wrap gap-1">
|
||||
<span class="px-2 py-0.5 bg-gray-100 text-gray-600 text-[10px] rounded-md font-medium">Kolacja</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div onclick="openRecipeDetail()" class="border border-gray-200 rounded-xl overflow-hidden shadow-sm flex flex-col bg-white cursor-pointer hover:shadow-md transition-shadow">
|
||||
<div class="h-32 bg-[#d4d4d4] relative flex items-center justify-center">
|
||||
<span class="text-white font-medium text-xs">Owsianka</span>
|
||||
</div>
|
||||
<div class="p-3 flex flex-col flex-1">
|
||||
<h3 class="text-sm font-medium underline decoration-1 underline-offset-2 text-black mb-1 line-clamp-1">Miska owsianki</h3>
|
||||
<p class="text-gray-500 text-xs mb-3 line-clamp-2">Ciepła owsianka z miodem i orzechami</p>
|
||||
<div class="mt-auto">
|
||||
<div class="flex items-center justify-between text-[11px] text-gray-600 font-medium mb-2">
|
||||
<div class="flex items-center gap-1"><i class="fas fa-clock text-gray-400"></i><span>10 min</span></div>
|
||||
<div class="flex items-center gap-1"><i class="fas fa-fire text-gray-400"></i><span>210 kcal</span></div>
|
||||
</div>
|
||||
<div class="flex flex-wrap gap-1">
|
||||
<span class="px-2 py-0.5 bg-gray-100 text-gray-600 text-[10px] rounded-md font-medium">Śniadanie</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
Reference in New Issue
Block a user