Cookbook — Browse / Search (cuisine · diet filters)
An editorial cookbook browse and search page where readers sift a fictional library of about twelve dishes by cuisine, diet, meal, and maximum cook time. A live search box, a faceted filter sidebar that folds into a mobile drawer, removable active-filter chips, a popular and quick and newest sort control, and a responsive grid of gradient recipe cards with ratings and a save heart all work together, complete with a graceful empty state.
MCP
Code
:root {
--cream: #faf6ef;
--paper: #fffdf8;
--ink: #2b2622;
--ink-2: #5c534a;
--muted: #8a7f73;
--tomato: #d6452b;
--tomato-d: #b8351e;
--saffron: #e8a33d;
--sage: #7c8a6b;
--clay: #c8775a;
--line: rgba(43, 38, 34, 0.12);
--line-2: rgba(43, 38, 34, 0.2);
--ok: #3f8f5f;
--warn: #d98a2b;
--danger: #c8412b;
--r-sm: 8px;
--r-md: 14px;
--r-lg: 22px;
--sh-1: 0 1px 2px rgba(43, 38, 34, 0.1);
--sh-2: 0 10px 30px rgba(43, 38, 34, 0.1);
--serif: "Fraunces", Georgia, serif;
--sans: "Inter", system-ui, -apple-system, sans-serif;
}
* {
box-sizing: border-box;
}
html {
-webkit-text-size-adjust: 100%;
}
body {
margin: 0;
background: var(--cream);
color: var(--ink);
font-family: var(--sans);
line-height: 1.6;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
.sr-only {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0 0 0 0);
white-space: nowrap;
border: 0;
}
:focus-visible {
outline: 2px solid var(--tomato);
outline-offset: 2px;
border-radius: 4px;
}
/* ---------- Header ---------- */
.site-head {
position: sticky;
top: 0;
z-index: 30;
background: rgba(255, 253, 248, 0.92);
backdrop-filter: saturate(1.2) blur(8px);
border-bottom: 1px solid var(--line);
}
.head-inner {
max-width: 1180px;
margin: 0 auto;
padding: 14px 22px;
display: flex;
align-items: center;
gap: 18px;
}
.brand {
display: flex;
align-items: center;
gap: 11px;
flex-shrink: 0;
}
.brand-mark {
font-size: 26px;
filter: saturate(1.2);
}
.brand-text {
display: flex;
flex-direction: column;
line-height: 1.05;
}
.kicker {
font-size: 10px;
letter-spacing: 0.18em;
text-transform: uppercase;
color: var(--muted);
font-weight: 600;
}
.brand-name {
font-family: var(--serif);
font-size: 22px;
font-weight: 600;
color: var(--ink);
}
.search {
position: relative;
flex: 1;
display: flex;
align-items: center;
}
.search-ico {
position: absolute;
left: 14px;
font-size: 14px;
opacity: 0.65;
pointer-events: none;
}
.search input {
width: 100%;
font-family: var(--sans);
font-size: 15px;
color: var(--ink);
background: var(--paper);
border: 1px solid var(--line-2);
border-radius: 999px;
padding: 11px 40px 11px 38px;
transition: border-color 0.18s, box-shadow 0.18s;
}
.search input::placeholder {
color: var(--muted);
}
.search input:focus {
outline: none;
border-color: var(--tomato);
box-shadow: 0 0 0 3px rgba(214, 69, 43, 0.14);
}
.search-clear {
position: absolute;
right: 8px;
border: none;
background: var(--line);
color: var(--ink-2);
width: 24px;
height: 24px;
border-radius: 50%;
cursor: pointer;
font-size: 12px;
line-height: 1;
display: grid;
place-items: center;
}
.search-clear:hover {
background: var(--line-2);
}
.drawer-toggle {
display: none;
align-items: center;
gap: 6px;
font-family: var(--sans);
font-size: 14px;
font-weight: 600;
color: var(--ink);
background: var(--paper);
border: 1px solid var(--line-2);
border-radius: 999px;
padding: 9px 14px;
cursor: pointer;
}
/* ---------- Layout ---------- */
.layout {
max-width: 1180px;
margin: 0 auto;
padding: 26px 22px 64px;
display: grid;
grid-template-columns: 248px 1fr;
gap: 34px;
align-items: start;
}
/* ---------- Sidebar ---------- */
.sidebar {
position: sticky;
top: 86px;
background: var(--paper);
border: 1px solid var(--line);
border-radius: var(--r-lg);
padding: 20px 20px 22px;
box-shadow: var(--sh-1);
}
.drawer-head {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 6px;
}
.sidebar-title {
font-family: var(--serif);
font-size: 19px;
font-weight: 600;
margin: 0;
}
.drawer-close {
display: none;
border: none;
background: transparent;
font-size: 18px;
color: var(--ink-2);
cursor: pointer;
}
.filter-group {
border: none;
border-top: 1px solid var(--line);
padding: 16px 0 4px;
margin: 0;
}
.filter-group legend {
font-size: 11px;
letter-spacing: 0.13em;
text-transform: uppercase;
color: var(--muted);
font-weight: 700;
padding: 0;
margin-bottom: 10px;
}
.opts {
display: flex;
flex-direction: column;
gap: 9px;
}
.opt {
display: flex;
align-items: center;
gap: 9px;
font-size: 14px;
color: var(--ink-2);
cursor: pointer;
user-select: none;
}
.opt input {
appearance: none;
-webkit-appearance: none;
width: 17px;
height: 17px;
border: 1.5px solid var(--line-2);
border-radius: 5px;
background: var(--cream);
cursor: pointer;
display: grid;
place-items: center;
flex-shrink: 0;
transition: background 0.15s, border-color 0.15s;
}
.opt input:checked {
background: var(--tomato);
border-color: var(--tomato);
}
.opt input:checked::after {
content: "✓";
color: #fff;
font-size: 11px;
font-weight: 700;
}
.opt:hover {
color: var(--ink);
}
.chips {
display: flex;
flex-wrap: wrap;
gap: 8px;
}
.chip {
font-family: var(--sans);
font-size: 13px;
font-weight: 500;
color: var(--ink-2);
background: var(--cream);
border: 1px solid var(--line-2);
border-radius: 999px;
padding: 6px 12px;
cursor: pointer;
transition: all 0.15s;
}
.chip:hover {
border-color: var(--clay);
color: var(--ink);
}
.chip[aria-checked="true"] {
background: var(--clay);
border-color: var(--clay);
color: #fff;
}
.reset-btn {
margin-top: 18px;
width: 100%;
font-family: var(--sans);
font-size: 13px;
font-weight: 600;
color: var(--tomato-d);
background: transparent;
border: 1px solid var(--line-2);
border-radius: var(--r-sm);
padding: 9px;
cursor: pointer;
transition: background 0.15s;
}
.reset-btn:hover {
background: rgba(214, 69, 43, 0.06);
}
/* ---------- Results bar ---------- */
.results-bar {
display: flex;
align-items: flex-end;
justify-content: space-between;
gap: 16px;
flex-wrap: wrap;
margin-bottom: 14px;
}
.page-title {
font-family: var(--serif);
font-size: 30px;
font-weight: 600;
margin: 0 0 2px;
letter-spacing: -0.01em;
}
.count {
margin: 0;
font-size: 13px;
color: var(--muted);
}
.sort select {
font-family: var(--sans);
font-size: 14px;
color: var(--ink);
background: var(--paper);
border: 1px solid var(--line-2);
border-radius: var(--r-sm);
padding: 9px 12px;
cursor: pointer;
}
.sort select:focus {
outline: none;
border-color: var(--tomato);
}
/* ---------- Active chips ---------- */
.active-chips {
display: flex;
flex-wrap: wrap;
gap: 8px;
margin-bottom: 18px;
min-height: 0;
}
.active-chips:empty {
margin-bottom: 0;
}
.act-chip {
display: inline-flex;
align-items: center;
gap: 7px;
font-size: 12.5px;
font-weight: 500;
color: var(--ink);
background: var(--paper);
border: 1px solid var(--line-2);
border-radius: 999px;
padding: 5px 8px 5px 12px;
}
.act-chip button {
border: none;
background: var(--line);
color: var(--ink-2);
width: 18px;
height: 18px;
border-radius: 50%;
font-size: 11px;
line-height: 1;
cursor: pointer;
display: grid;
place-items: center;
}
.act-chip button:hover {
background: var(--tomato);
color: #fff;
}
/* ---------- Grid + cards ---------- */
.grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(228px, 1fr));
gap: 22px;
}
.card {
background: var(--paper);
border: 1px solid var(--line);
border-radius: var(--r-lg);
overflow: hidden;
box-shadow: var(--sh-1);
display: flex;
flex-direction: column;
transition: transform 0.18s, box-shadow 0.18s;
}
.card:hover {
transform: translateY(-3px);
box-shadow: var(--sh-2);
}
.card-photo {
position: relative;
aspect-ratio: 4 / 3;
display: grid;
place-items: center;
font-size: 46px;
overflow: hidden;
}
.card-photo::after {
content: "";
position: absolute;
inset: 0;
background: radial-gradient(circle at 70% 18%, rgba(255, 255, 255, 0.35), transparent 42%);
pointer-events: none;
}
.card-photo .emoji {
filter: drop-shadow(0 6px 12px rgba(43, 38, 34, 0.25));
transform: translateY(-2px);
}
.heart {
position: absolute;
top: 10px;
right: 10px;
z-index: 2;
width: 34px;
height: 34px;
border-radius: 50%;
border: none;
background: rgba(255, 253, 248, 0.9);
backdrop-filter: blur(4px);
cursor: pointer;
font-size: 15px;
display: grid;
place-items: center;
box-shadow: var(--sh-1);
transition: transform 0.15s;
}
.heart:hover {
transform: scale(1.1);
}
.heart[aria-pressed="true"] {
background: var(--tomato);
}
.time-pill {
position: absolute;
bottom: 10px;
left: 10px;
z-index: 2;
font-size: 11.5px;
font-weight: 600;
color: var(--ink);
background: rgba(255, 253, 248, 0.92);
border-radius: 999px;
padding: 3px 9px;
box-shadow: var(--sh-1);
}
.card-body {
padding: 14px 15px 16px;
display: flex;
flex-direction: column;
gap: 9px;
flex: 1;
}
.card-cuisine {
font-size: 10.5px;
letter-spacing: 0.12em;
text-transform: uppercase;
color: var(--clay);
font-weight: 700;
}
.card-title {
font-family: var(--serif);
font-size: 18px;
font-weight: 600;
line-height: 1.25;
margin: 0;
color: var(--ink);
}
.card-desc {
font-size: 13px;
color: var(--ink-2);
margin: 0;
flex: 1;
}
.badges {
display: flex;
flex-wrap: wrap;
gap: 6px;
}
.badge {
font-size: 11px;
font-weight: 600;
color: var(--sage);
background: rgba(124, 138, 107, 0.13);
border-radius: 999px;
padding: 3px 9px;
}
.card-foot {
display: flex;
align-items: center;
justify-content: space-between;
border-top: 1px solid var(--line);
padding-top: 10px;
margin-top: 2px;
}
.rating {
display: flex;
align-items: center;
gap: 5px;
font-size: 13px;
font-weight: 600;
color: var(--ink);
}
.rating .stars {
color: var(--saffron);
letter-spacing: 1px;
font-size: 12px;
}
.rating .num {
color: var(--muted);
font-weight: 500;
}
.serves {
font-size: 12px;
color: var(--muted);
}
/* ---------- Empty state ---------- */
.empty {
text-align: center;
padding: 60px 20px;
color: var(--ink-2);
}
.empty-art {
font-size: 56px;
margin-bottom: 10px;
opacity: 0.8;
}
.empty h2 {
font-family: var(--serif);
font-size: 22px;
font-weight: 600;
margin: 0 0 6px;
color: var(--ink);
}
.empty p {
margin: 0 0 18px;
}
.empty .reset-btn {
width: auto;
display: inline-block;
padding: 10px 22px;
}
/* ---------- Scrim + toast ---------- */
.scrim {
position: fixed;
inset: 0;
background: rgba(43, 38, 34, 0.4);
z-index: 40;
}
.toast {
position: fixed;
bottom: 24px;
left: 50%;
transform: translate(-50%, 20px);
background: var(--ink);
color: var(--paper);
font-size: 13.5px;
font-weight: 500;
padding: 11px 18px;
border-radius: 999px;
box-shadow: var(--sh-2);
opacity: 0;
pointer-events: none;
transition: opacity 0.2s, transform 0.2s;
z-index: 60;
}
.toast.show {
opacity: 1;
transform: translate(-50%, 0);
}
/* ---------- Responsive ---------- */
@media (max-width: 860px) {
.layout {
grid-template-columns: 1fr;
}
.drawer-toggle {
display: inline-flex;
}
.drawer-close {
display: block;
}
.sidebar {
position: fixed;
top: 0;
left: 0;
bottom: 0;
width: min(320px, 86vw);
border-radius: 0;
border: none;
border-right: 1px solid var(--line);
z-index: 50;
transform: translateX(-100%);
transition: transform 0.26s ease;
overflow-y: auto;
box-shadow: var(--sh-2);
}
.sidebar.open {
transform: translateX(0);
}
}
@media (max-width: 720px) {
.head-inner {
flex-wrap: wrap;
padding: 12px 16px;
}
.search {
order: 3;
flex-basis: 100%;
}
.layout {
padding: 20px 16px 56px;
}
.page-title {
font-size: 25px;
}
}
@media (max-width: 420px) {
.grid {
grid-template-columns: 1fr 1fr;
gap: 14px;
}
.card-photo {
font-size: 36px;
}
.card-title {
font-size: 15px;
}
.card-desc {
display: none;
}
}(function () {
"use strict";
// ---------- Fictional dataset (~12 recipes) ----------
var RECIPES = [
{
id: 1, title: "Charred Tomato Bucatini", cuisine: "Italian",
diet: ["Vegetarian"], meal: "Dinner", time: 35, rating: 4.8, reviews: 214,
serves: 4, emoji: "🍝", grad: "linear-gradient(135deg,#e8643c,#d6452b 70%)",
desc: "Blistered cherry tomatoes, garlic confit, torn basil.", added: 1717200000000
},
{
id: 2, title: "Street-Corn Tacos", cuisine: "Mexican",
diet: ["Vegetarian", "Gluten-free"], meal: "Lunch", time: 25, rating: 4.7, reviews: 168,
serves: 3, emoji: "🌮", grad: "linear-gradient(135deg,#e8a33d,#c8775a 75%)",
desc: "Smoky charred corn, lime crema, cotija, chili.", added: 1718500000000
},
{
id: 3, title: "Miso Salmon Donburi", cuisine: "Japanese",
diet: ["High-protein"], meal: "Dinner", time: 30, rating: 4.9, reviews: 302,
serves: 2, emoji: "🍣", grad: "linear-gradient(135deg,#c8775a,#7c8a6b 80%)",
desc: "Glazed salmon, steamed rice, pickled ginger, scallion.", added: 1719000000000
},
{
id: 4, title: "Golden Chana Masala", cuisine: "Indian",
diet: ["Vegan", "Gluten-free", "High-protein"], meal: "Dinner", time: 40, rating: 4.6, reviews: 191,
serves: 4, emoji: "🍛", grad: "linear-gradient(135deg,#e8a33d,#d6452b 78%)",
desc: "Slow-spiced chickpeas, tomato, ginger, fresh cilantro.", added: 1716000000000
},
{
id: 5, title: "Lemon-Herb Greek Bowl", cuisine: "Mediterranean",
diet: ["Vegetarian", "Gluten-free"], meal: "Lunch", time: 20, rating: 4.5, reviews: 132,
serves: 2, emoji: "🥗", grad: "linear-gradient(135deg,#7c8a6b,#e8a33d 85%)",
desc: "Whipped feta, olives, cucumber, oregano-lemon dressing.", added: 1719500000000
},
{
id: 6, title: "Buttermilk Stack Pancakes", cuisine: "American",
diet: ["Vegetarian"], meal: "Breakfast", time: 18, rating: 4.7, reviews: 256,
serves: 3, emoji: "🥞", grad: "linear-gradient(135deg,#e8a33d,#c8775a 70%)",
desc: "Tall, fluffy stacks with maple butter and berries.", added: 1715000000000
},
{
id: 7, title: "Forest Mushroom Risotto", cuisine: "Italian",
diet: ["Vegetarian"], meal: "Dinner", time: 45, rating: 4.8, reviews: 178,
serves: 4, emoji: "🍚", grad: "linear-gradient(135deg,#8a7f73,#5c534a 85%)",
desc: "Creamy arborio, wild mushrooms, parmesan, thyme.", added: 1714000000000
},
{
id: 8, title: "Crunchy Avocado Toast", cuisine: "American",
diet: ["Vegan", "Vegetarian"], meal: "Breakfast", time: 12, rating: 4.4, reviews: 98,
serves: 1, emoji: "🥑", grad: "linear-gradient(135deg,#7c8a6b,#a9b88e 85%)",
desc: "Smashed avocado, chili crisp, radish, sea salt.", added: 1719800000000
},
{
id: 9, title: "Spicy Tuna Onigiri", cuisine: "Japanese",
diet: ["High-protein"], meal: "Lunch", time: 22, rating: 4.6, reviews: 144,
serves: 2, emoji: "🍙", grad: "linear-gradient(135deg,#5c534a,#7c8a6b 80%)",
desc: "Seasoned rice triangles, spicy tuna, toasted nori.", added: 1718000000000
},
{
id: 10, title: "Salted Caramel Tartlets", cuisine: "Mediterranean",
diet: ["Vegetarian"], meal: "Dessert", time: 55, rating: 4.9, reviews: 221,
serves: 6, emoji: "🍮", grad: "linear-gradient(135deg,#c8775a,#e8a33d 80%)",
desc: "Buttery shells, dark caramel, flaked Maldon salt.", added: 1713000000000
},
{
id: 11, title: "Mango Sticky Rice", cuisine: "Mexican",
diet: ["Vegan", "Vegetarian", "Gluten-free"], meal: "Dessert", time: 38, rating: 4.5, reviews: 87,
serves: 4, emoji: "🥭", grad: "linear-gradient(135deg,#e8a33d,#7c8a6b 88%)",
desc: "Coconut-sweet rice, ripe mango, toasted sesame.", added: 1717800000000
},
{
id: 12, title: "Smoky Black Bean Chili", cuisine: "Mexican",
diet: ["Vegan", "Gluten-free", "High-protein"], meal: "Dinner", time: 50, rating: 4.7, reviews: 203,
serves: 6, emoji: "🌶️", grad: "linear-gradient(135deg,#d6452b,#b8351e 80%)",
desc: "Three beans, smoked paprika, chipotle, lime finish.", added: 1716500000000
}
];
// ---------- State ----------
var state = {
q: "",
cuisine: [], diet: [], meal: [],
maxTime: 0,
sort: "popular",
saved: {}
};
var $ = function (s, ctx) { return (ctx || document).querySelector(s); };
var $$ = function (s, ctx) { return Array.prototype.slice.call((ctx || document).querySelectorAll(s)); };
var grid = $("#grid");
var empty = $("#empty");
var countEl = $("#resultCount");
var activeChips = $("#activeChips");
var FACET_LABELS = { cuisine: "Cuisine", diet: "Diet", meal: "Meal" };
// ---------- Toast ----------
var toastEl = $("#toast");
var toastTimer;
function toast(msg) {
toastEl.textContent = msg;
toastEl.classList.add("show");
clearTimeout(toastTimer);
toastTimer = setTimeout(function () { toastEl.classList.remove("show"); }, 2200);
}
// ---------- Filtering + sort ----------
function matches(r) {
if (state.q) {
var hay = (r.title + " " + r.cuisine + " " + r.desc + " " + r.diet.join(" ") + " " + r.meal).toLowerCase();
if (hay.indexOf(state.q.toLowerCase()) === -1) return false;
}
if (state.cuisine.length && state.cuisine.indexOf(r.cuisine) === -1) return false;
if (state.meal.length && state.meal.indexOf(r.meal) === -1) return false;
if (state.diet.length) {
var ok = state.diet.every(function (d) { return r.diet.indexOf(d) !== -1; });
if (!ok) return false;
}
if (state.maxTime && r.time > state.maxTime) return false;
return true;
}
function sortList(list) {
var copy = list.slice();
if (state.sort === "popular") {
copy.sort(function (a, b) { return b.rating - a.rating || b.reviews - a.reviews; });
} else if (state.sort === "quick") {
copy.sort(function (a, b) { return a.time - b.time; });
} else if (state.sort === "newest") {
copy.sort(function (a, b) { return b.added - a.added; });
}
return copy;
}
function stars(rating) {
var full = Math.round(rating);
var s = "";
for (var i = 0; i < 5; i++) s += i < full ? "★" : "☆";
return s;
}
function cardHTML(r) {
var saved = !!state.saved[r.id];
var badges = r.diet.map(function (d) { return '<span class="badge">' + d + "</span>"; }).join("");
return (
'<article class="card" data-id="' + r.id + '">' +
'<div class="card-photo" style="background:' + r.grad + '">' +
'<button class="heart" type="button" aria-pressed="' + saved + '" aria-label="Save ' + r.title + '">' +
(saved ? "❤️" : "🤍") +
"</button>" +
'<span class="emoji" aria-hidden="true">' + r.emoji + "</span>" +
'<span class="time-pill">⏱ ' + r.time + " min</span>" +
"</div>" +
'<div class="card-body">' +
'<span class="card-cuisine">' + r.cuisine + "</span>" +
'<h3 class="card-title">' + r.title + "</h3>" +
'<p class="card-desc">' + r.desc + "</p>" +
'<div class="badges">' + badges + "</div>" +
'<div class="card-foot">' +
'<span class="rating"><span class="stars" aria-hidden="true">' + stars(r.rating) + "</span>" +
r.rating.toFixed(1) + ' <span class="num">(' + r.reviews + ")</span></span>" +
'<span class="serves">Serves ' + r.serves + "</span>" +
"</div>" +
"</div>" +
"</article>"
);
}
function render() {
var list = sortList(RECIPES.filter(matches));
countEl.textContent =
list.length + (list.length === 1 ? " recipe" : " recipes") +
(hasFilters() ? " match your filters" : " in the cookbook");
if (!list.length) {
grid.innerHTML = "";
empty.hidden = false;
} else {
empty.hidden = true;
grid.innerHTML = list.map(cardHTML).join("");
}
renderActiveChips();
}
function hasFilters() {
return !!(state.q || state.cuisine.length || state.diet.length || state.meal.length || state.maxTime);
}
function renderActiveChips() {
var chips = [];
if (state.q) chips.push({ facet: "q", value: state.q, label: '“' + state.q + '”' });
["cuisine", "diet", "meal"].forEach(function (f) {
state[f].forEach(function (v) { chips.push({ facet: f, value: v, label: v }); });
});
if (state.maxTime) chips.push({ facet: "maxTime", value: state.maxTime, label: "≤ " + state.maxTime + " min" });
activeChips.innerHTML = chips.map(function (c) {
return '<span class="act-chip">' + c.label +
'<button type="button" data-facet="' + c.facet + '" data-value="' + c.value +
'" aria-label="Remove ' + c.label + '">✕</button></span>';
}).join("");
}
// ---------- Active chip removal ----------
activeChips.addEventListener("click", function (e) {
var btn = e.target.closest("button");
if (!btn) return;
var facet = btn.getAttribute("data-facet");
var value = btn.getAttribute("data-value");
if (facet === "q") {
state.q = "";
$("#searchInput").value = "";
$("#searchClear").hidden = true;
} else if (facet === "maxTime") {
state.maxTime = 0;
syncTimeChips();
} else {
state[facet] = state[facet].filter(function (v) { return v !== value; });
syncCheckboxes(facet);
}
render();
});
// ---------- Save heart ----------
grid.addEventListener("click", function (e) {
var heart = e.target.closest(".heart");
if (!heart) return;
var id = +heart.closest(".card").getAttribute("data-id");
state.saved[id] = !state.saved[id];
heart.setAttribute("aria-pressed", String(!!state.saved[id]));
heart.textContent = state.saved[id] ? "❤️" : "🤍";
var r = RECIPES.filter(function (x) { return x.id === id; })[0];
toast(state.saved[id] ? "Saved “" + r.title + "” to your recipe box" : "Removed from saved");
});
// ---------- Search ----------
var searchInput = $("#searchInput");
var searchClear = $("#searchClear");
searchInput.addEventListener("input", function () {
state.q = searchInput.value.trim();
searchClear.hidden = !searchInput.value;
render();
});
$("#searchForm").addEventListener("submit", function (e) { e.preventDefault(); });
searchClear.addEventListener("click", function () {
searchInput.value = "";
state.q = "";
searchClear.hidden = true;
searchInput.focus();
render();
});
// ---------- Checkbox facets ----------
$$(".opts").forEach(function (group) {
var facet = group.getAttribute("data-facet");
group.addEventListener("change", function (e) {
var cb = e.target;
if (cb.checked) {
if (state[facet].indexOf(cb.value) === -1) state[facet].push(cb.value);
} else {
state[facet] = state[facet].filter(function (v) { return v !== cb.value; });
}
render();
});
});
function syncCheckboxes(facet) {
$$('.opts[data-facet="' + facet + '"] input').forEach(function (cb) {
cb.checked = state[facet].indexOf(cb.value) !== -1;
});
}
// ---------- Time chips (radio) ----------
var timeChips = $$('.chips[data-facet="maxTime"] .chip');
timeChips.forEach(function (chip) {
chip.addEventListener("click", function () {
var t = +chip.getAttribute("data-time");
state.maxTime = t === state.maxTime ? 0 : t;
syncTimeChips();
render();
});
});
function syncTimeChips() {
timeChips.forEach(function (chip) {
var on = +chip.getAttribute("data-time") === state.maxTime && state.maxTime !== 0;
chip.setAttribute("aria-checked", String(on));
});
}
// ---------- Sort ----------
$("#sortSelect").addEventListener("change", function (e) {
state.sort = e.target.value;
render();
});
// ---------- Reset ----------
function resetAll() {
state.q = "";
state.cuisine = [];
state.diet = [];
state.meal = [];
state.maxTime = 0;
searchInput.value = "";
searchClear.hidden = true;
$$(".opts input").forEach(function (cb) { cb.checked = false; });
syncTimeChips();
render();
toast("Filters cleared");
}
$("#resetAll").addEventListener("click", resetAll);
$("#emptyReset").addEventListener("click", function () { resetAll(); closeDrawer(); });
// ---------- Mobile drawer ----------
var sidebar = $("#filters");
var scrim = $("#scrim");
var drawerToggle = $("#drawerToggle");
function openDrawer() {
sidebar.classList.add("open");
scrim.hidden = false;
drawerToggle.setAttribute("aria-expanded", "true");
}
function closeDrawer() {
sidebar.classList.remove("open");
scrim.hidden = true;
drawerToggle.setAttribute("aria-expanded", "false");
}
drawerToggle.addEventListener("click", openDrawer);
$("#drawerClose").addEventListener("click", closeDrawer);
scrim.addEventListener("click", closeDrawer);
document.addEventListener("keydown", function (e) {
if (e.key === "Escape" && sidebar.classList.contains("open")) closeDrawer();
});
// ---------- Init ----------
syncTimeChips();
render();
})();<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Cookbook — Browse & Search</title>
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link href="https://fonts.googleapis.com/css2?family=Fraunces:opsz,[email protected],500;9..144,600;9..144,700&family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet" />
<link rel="stylesheet" href="style.css" />
</head>
<body>
<header class="site-head" role="banner">
<div class="head-inner">
<div class="brand">
<span class="brand-mark" aria-hidden="true">🍅</span>
<div class="brand-text">
<span class="kicker">The Stealthis</span>
<span class="brand-name">Cookbook</span>
</div>
</div>
<form class="search" role="search" id="searchForm">
<span class="search-ico" aria-hidden="true">🔍</span>
<label class="sr-only" for="searchInput">Search recipes</label>
<input
type="search"
id="searchInput"
name="q"
placeholder="Search dishes, ingredients, cuisines…"
autocomplete="off"
spellcheck="false"
/>
<button type="button" class="search-clear" id="searchClear" aria-label="Clear search" hidden>✕</button>
</form>
<button type="button" class="drawer-toggle" id="drawerToggle" aria-expanded="false" aria-controls="filters">
<span aria-hidden="true">⚙️</span> Filters
</button>
</div>
</header>
<div class="layout">
<!-- Filter sidebar / drawer -->
<aside class="sidebar" id="filters" aria-label="Recipe filters">
<div class="drawer-head">
<h2 class="sidebar-title">Refine</h2>
<button type="button" class="drawer-close" id="drawerClose" aria-label="Close filters">✕</button>
</div>
<fieldset class="filter-group">
<legend>Cuisine</legend>
<div class="opts" data-facet="cuisine">
<label class="opt"><input type="checkbox" value="Italian" /> <span>Italian 🇮🇹</span></label>
<label class="opt"><input type="checkbox" value="Mexican" /> <span>Mexican 🌶️</span></label>
<label class="opt"><input type="checkbox" value="Japanese" /> <span>Japanese 🍙</span></label>
<label class="opt"><input type="checkbox" value="Indian" /> <span>Indian 🍛</span></label>
<label class="opt"><input type="checkbox" value="Mediterranean" /> <span>Mediterranean 🫒</span></label>
<label class="opt"><input type="checkbox" value="American" /> <span>American 🥧</span></label>
</div>
</fieldset>
<fieldset class="filter-group">
<legend>Diet</legend>
<div class="opts" data-facet="diet">
<label class="opt"><input type="checkbox" value="Vegetarian" /> <span>Vegetarian 🥕</span></label>
<label class="opt"><input type="checkbox" value="Vegan" /> <span>Vegan 🌿</span></label>
<label class="opt"><input type="checkbox" value="Gluten-free" /> <span>Gluten-free 🌾</span></label>
<label class="opt"><input type="checkbox" value="High-protein" /> <span>High-protein 💪</span></label>
</div>
</fieldset>
<fieldset class="filter-group">
<legend>Meal</legend>
<div class="opts" data-facet="meal">
<label class="opt"><input type="checkbox" value="Breakfast" /> <span>Breakfast 🍳</span></label>
<label class="opt"><input type="checkbox" value="Lunch" /> <span>Lunch 🥗</span></label>
<label class="opt"><input type="checkbox" value="Dinner" /> <span>Dinner 🍝</span></label>
<label class="opt"><input type="checkbox" value="Dessert" /> <span>Dessert 🍰</span></label>
</div>
</fieldset>
<fieldset class="filter-group">
<legend>Max time</legend>
<div class="chips" role="radiogroup" aria-label="Maximum cook time" data-facet="maxTime">
<button type="button" class="chip" role="radio" aria-checked="false" data-time="20">≤ 20 min</button>
<button type="button" class="chip" role="radio" aria-checked="false" data-time="40">≤ 40 min</button>
<button type="button" class="chip" role="radio" aria-checked="false" data-time="60">≤ 60 min</button>
<button type="button" class="chip" role="radio" aria-checked="false" data-time="0">Any</button>
</div>
</fieldset>
<button type="button" class="reset-btn" id="resetAll">Clear all filters</button>
</aside>
<main class="main" role="main">
<div class="results-bar">
<div class="results-meta">
<h1 class="page-title">Browse recipes</h1>
<p class="count" id="resultCount" aria-live="polite">Loading…</p>
</div>
<label class="sort">
<span class="sr-only">Sort recipes</span>
<select id="sortSelect">
<option value="popular">Most popular</option>
<option value="quick">Quickest first</option>
<option value="newest">Newest</option>
</select>
</label>
</div>
<div class="active-chips" id="activeChips" aria-live="polite"></div>
<section class="grid" id="grid" aria-label="Recipe results"></section>
<div class="empty" id="empty" hidden>
<div class="empty-art" aria-hidden="true">🍽️</div>
<h2>No recipes match those filters</h2>
<p>Try widening your search or clearing a filter or two.</p>
<button type="button" class="reset-btn" id="emptyReset">Clear all filters</button>
</div>
</main>
</div>
<div class="scrim" id="scrim" hidden></div>
<div class="toast" id="toast" role="status" aria-live="polite"></div>
<script src="script.js"></script>
</body>
</html>Browse / Search (cuisine · diet filters)
A warm, print-inspired recipe browser. A sticky header carries the cookbook brand and a live search box that matches across titles, cuisines, diets, and descriptions. The left sidebar offers faceted filters — cuisine and diet and meal checkboxes plus a set of max-time chips — and every active choice surfaces as a removable chip above the results so it is always clear why a recipe is or is not showing.
The responsive grid renders gradient “photo” cards (no images, just CSS and food emoji) with cuisine kickers, diet badges, star ratings, serving counts, and a save heart that toasts when toggled. A sort control reorders by most popular, quickest, or newest, and the live result count updates with every keystroke. When nothing matches, a friendly empty state invites you to widen the search. On narrow screens the sidebar collapses into a slide-in filter drawer with a scrim and Escape-to-close.
Everything runs on vanilla JavaScript over a single in-memory dataset, so it is easy to drop in real recipes or wire to an API later.
Illustrative UI only — recipes & nutrition data are fictional, not dietary advice.