сборщик кофе: настроение, основа/молоко/сироп/размер/поподача, живой стикер и цена

This commit is contained in:
ALMAGUL 2026-09-25 10:00:05 +00:00
parent 4d61a313ad
commit 91571b139f
2 changed files with 433 additions and 3 deletions

View File

@ -10,13 +10,14 @@
<link rel="stylesheet" href="design-system/kt-ai-components.css"> <link rel="stylesheet" href="design-system/kt-ai-components.css">
<link rel="stylesheet" href="design-system/kt-ai-page.css"> <link rel="stylesheet" href="design-system/kt-ai-page.css">
<link rel="stylesheet" href="design-system/vibe-theme.css"> <link rel="stylesheet" href="design-system/vibe-theme.css">
<script src="design-system/icons/kt-ai-icons.js"></script>
<style> <style>
a.kt-ai-btn { text-decoration: none; } a.kt-ai-btn { text-decoration: none; }
.menu-photo { .menu-photo {
position: relative; position: relative;
margin-top: var(--kt-ai-space-6); margin-top: var(--kt-ai-space-6);
border-radius: var(--kt-ai-radius-lg, 12px); border-radius: var(--kt-ai-radius-lg);
overflow: hidden; overflow: hidden;
border: 1px solid var(--kt-ai-border); border: 1px solid var(--kt-ai-border);
background: var(--kt-ai-bg-soft); background: var(--kt-ai-bg-soft);
@ -64,6 +65,122 @@
gap: var(--kt-ai-space-3); gap: var(--kt-ai-space-3);
margin-top: var(--kt-ai-space-6); margin-top: var(--kt-ai-space-6);
} }
/* ── Сборка кофе ── */
.mood-row {
display: grid;
gap: var(--kt-ai-space-3);
margin-top: var(--kt-ai-space-8);
grid-template-columns: repeat(3, 1fr);
}
@media (max-width: 700px) { .mood-row { grid-template-columns: 1fr; } }
.mood-btn {
text-align: left;
padding: var(--kt-ai-space-4) var(--kt-ai-space-5);
border-radius: var(--kt-ai-radius-md);
border: 1px solid var(--kt-ai-border);
background: var(--kt-ai-bg);
color: var(--kt-ai-fg);
font: inherit;
cursor: pointer;
min-height: 88px;
display: flex;
flex-direction: column;
gap: 2px;
}
.mood-btn:hover { border-color: var(--kt-ai-border-strong); }
.mood-btn b { font-weight: 600; }
.mood-btn span { font-size: 0.85rem; color: var(--kt-ai-fg-muted); }
.builder {
margin-top: var(--kt-ai-space-8);
border: 1px solid var(--kt-ai-border);
border-radius: var(--kt-ai-radius-lg);
background: var(--kt-ai-bg);
padding: var(--kt-ai-space-6);
}
.builder[hidden] { display: none; }
.builder-grid {
display: grid;
gap: var(--kt-ai-space-6);
grid-template-columns: 1.1fr 1fr;
}
@media (max-width: 900px) { .builder-grid { grid-template-columns: 1fr; } }
.field-label {
display: block;
font-size: 0.8rem;
font-weight: 500;
text-transform: uppercase;
letter-spacing: 0.06em;
color: var(--kt-ai-fg-muted);
margin-bottom: var(--kt-ai-space-3);
}
.opt-row { display: grid; gap: var(--kt-ai-space-2); }
.opt-row.cols-2 { grid-template-columns: repeat(2, 1fr); }
.opt-row.cols-3 { grid-template-columns: repeat(3, 1fr); }
.opt {
position: relative;
padding: 0;
margin: 0;
width: 100%;
text-align: left;
font-family: inherit;
font-size: 0.9rem;
border: 1px solid var(--kt-ai-border);
border-radius: var(--kt-ai-radius-md);
background: var(--kt-ai-bg);
color: var(--kt-ai-fg);
cursor: pointer;
min-height: 48px;
display: flex;
align-items: center;
gap: var(--kt-ai-space-2);
padding: var(--kt-ai-space-2) var(--kt-ai-space-3);
line-height: 1.35;
}
.opt .opt-label { flex: 1; }
.opt .opt-price { white-space: nowrap; font-size: 0.8rem; color: var(--kt-ai-fg-muted); }
.opt:focus-visible { outline: none; box-shadow: var(--kt-ai-focus-shadow); }
.opt input { position: absolute; opacity: 0; pointer-events: none; }
.opt:has(input:checked) {
border-color: var(--kt-ai-primary);
box-shadow: 0 0 0 1px var(--kt-ai-primary) inset;
background: var(--kt-ai-primary-subtle);
}
.opt:has(input:focus-visible) { box-shadow: var(--kt-ai-focus-shadow); }
.cup-panel {
display: flex;
flex-direction: column;
align-items: center;
gap: var(--kt-ai-space-5);
}
.cup-svg { width: 220px; max-width: 100%; height: auto; }
.cup-svg .part { transition: fill 0.25s, opacity 0.25s; }
.build-name {
font-size: 1.4rem;
font-weight: 500;
text-align: center;
line-height: 1.3;
margin: 0;
}
.build-summary {
display: grid;
gap: 4px;
font-size: 0.85rem;
color: var(--kt-ai-fg-muted);
text-align: center;
}
.build-price {
font-size: 1.9rem;
font-weight: 600;
letter-spacing: -0.02em;
margin: 0;
}
.build-price small { font-size: 0.9rem; font-weight: 400; color: var(--kt-ai-fg-muted); }
</style> </style>
</head> </head>
<body> <body>
@ -74,6 +191,7 @@
<span class="kt-ai-spacer"></span> <span class="kt-ai-spacer"></span>
<a class="kt-ai-btn" data-variant="ghost" href="#about">О нас</a> <a class="kt-ai-btn" data-variant="ghost" href="#about">О нас</a>
<a class="kt-ai-btn" data-variant="ghost" href="#menu">Меню</a> <a class="kt-ai-btn" data-variant="ghost" href="#menu">Меню</a>
<a class="kt-ai-btn" data-variant="ghost" href="#builder">Собрать кофе</a>
<a class="kt-ai-btn" data-variant="ghost" href="https://instagram.com/zero.coffee.kz" target="_blank" rel="noopener">Instagram</a> <a class="kt-ai-btn" data-variant="ghost" href="https://instagram.com/zero.coffee.kz" target="_blank" rel="noopener">Instagram</a>
<a class="kt-ai-btn" data-variant="primary" href="#contacts">Как нас найти</a> <a class="kt-ai-btn" data-variant="primary" href="#contacts">Как нас найти</a>
</div> </div>
@ -84,8 +202,72 @@
<h1>Кофе, за которым <span style="background:var(--kt-ai-primary-subtle);padding:0 0.4em;border-radius:6px">возвращаются</span></h1> <h1>Кофе, за которым <span style="background:var(--kt-ai-primary-subtle);padding:0 0.4em;border-radius:6px">возвращаются</span></h1>
<p class="kt-ai-hero-sub">Свежая обжарка, тихие углы и выпечка каждое утро — кофейня рядом с домом.</p> <p class="kt-ai-hero-sub">Свежая обжарка, тихие углы и выпечка каждое утро — кофейня рядом с домом.</p>
<div class="kt-ai-hero-actions"> <div class="kt-ai-hero-actions">
<a class="kt-ai-btn" data-variant="primary" data-size="lg" href="#menu">Смотреть меню</a> <a class="kt-ai-btn" data-variant="primary" data-size="lg" href="#builder">Собрать свой кофе</a>
<a class="kt-ai-btn" data-size="lg" href="#contacts">Как нас найти</a> <a class="kt-ai-btn" data-size="lg" href="#menu">Смотреть меню</a>
</div>
</div>
</section>
<section id="builder" class="kt-ai-section" data-divider="true">
<div class="kt-ai-wrap">
<div class="kt-ai-section-head">
<h2>Соберите свой кофе</h2>
<p>Сначала — настроение подберёт базовый рецепт. Дальше — настраивайте под себя: итоговая цена меняется на ходу.</p>
</div>
<div class="mood-row" id="moodRow">
<button type="button" class="mood-btn" data-mood="wake">
<b>Хочу проснуться</b>
<span>крепкий кофе без лишнего</span>
</button>
<button type="button" class="mood-btn" data-mood="soft">
<b>Хочу что-то нежное</b>
<span>бархатный латте</span>
</button>
<button type="button" class="mood-btn" data-mood="light">
<b>Что-нибудь лёгкое</b>
<span>травяной чай без кофеина</span>
</button>
</div>
<div class="builder" id="builderCard" hidden>
<div class="builder-grid">
<div>
<label class="field-label" for="opt-base">1. Основа</label>
<div class="opt-row" id="group-base"></div>
<label class="field-label" style="margin-top:var(--kt-ai-space-6)" for="opt-milk">2. Молоко</label>
<div class="opt-row" id="group-milk"></div>
<label class="field-label" style="margin-top:var(--kt-ai-space-6)" for="opt-syrup">3. Сироп</label>
<div class="opt-row" id="group-syrup"></div>
<label class="field-label" style="margin-top:var(--kt-ai-space-6)" for="opt-size">4. Размер</label>
<div class="opt-row cols-3" id="group-size"></div>
<label class="field-label" style="margin-top:var(--kt-ai-space-6)" for="opt-temp">5. Как подать</label>
<div class="opt-row cols-3" id="group-temp"></div>
</div>
<div class="cup-panel">
<svg class="cup-svg" viewBox="0 0 200 240" aria-hidden="true">
<g id="steam" opacity="0" style="transition:opacity .25s">
<path d="M84 22c-6 9 6 9 0 18" stroke="var(--kt-ai-fg-faint)" stroke-width="4" fill="none" stroke-linecap="round"/>
<path d="M116 16c-6 9 6 9 0 18" stroke="var(--kt-ai-fg-faint)" stroke-width="4" fill="none" stroke-linecap="round"/>
</g>
<g id="iceGroup"></g>
<rect id="cupInner" x="38" y="46" width="124" height="168" rx="14" fill="var(--kt-ai-bg-soft)" />
<g id="layers"></g>
<path d="M162 76c22 2 24 16 24 26s-8 22-28 20" stroke="var(--kt-ai-fg-faint)" stroke-width="6" fill="none" stroke-linecap="round"/>
<rect id="cupBody" x="38" y="46" width="124" height="168" rx="14" fill="none" stroke="var(--kt-ai-fg-faint)" stroke-width="6"/>
<ellipse cx="100" cy="224" rx="64" ry="7" fill="var(--kt-ai-border)"/>
</svg>
<p class="build-name" id="buildName">—</p>
<div class="build-summary" id="buildSummary"></div>
<p class="build-price" id="buildPrice"><small>Итого</small> 1 450 ₸</p>
</div>
</div>
</div> </div>
</div> </div>
</section> </section>
@ -226,5 +408,6 @@
</span> </span>
</footer> </footer>
<script src="script.js"></script>
</body> </body>
</html> </html>

247
script.js Normal file
View File

@ -0,0 +1,247 @@
const PRODUCTS = {
base: [
{ id: "espresso", label: "Эспрессо", price: 900, hasMilk: false, drink: true },
{ id: "americano", label: "Американо", price: 1100, hasMilk: false, drink: true },
{ id: "latte", label: "Латте (база)", price: 900, hasMilk: true, drink: true },
{ id: "mocha", label: "Мока", price: 1100, hasMilk: true, drink: true },
{ id: "herb", label: "Травяной чай", price: 800, hasMilk: false, drink: true },
],
milk: [
{ id: "none", label: "Без молока", price: 0 },
{ id: "milk", label: "Коровье", price: 100 },
{ id: "oat", label: "Овсяное", price: 300 },
{ id: "almond", label: "Миндальное", price: 300 },
{ id: "coconut", label: "Кокосовое", price: 350 },
],
syrup: [
{ id: "none", label: "Без сиропа", price: 0 },
{ id: "vanilla", label: "Ваниль", price: 300 },
{ id: "caramel", label: "Карамель", price: 300 },
{ id: "lavender", label: "Лаванда", price: 350 },
{ id: "honey", label: "Мёд", price: 350 },
],
size: [
{ id: "s", label: "250 мл", price: 0 },
{ id: "m", label: "350 мл", price: 300 },
{ id: "l", label: "450 мл", price: 500 },
],
temp: [
{ id: "hot", label: "Горячий", price: 0 },
{ id: "iced", label: "На льду", price: 200 },
],
};
const BASE_COLORS = {
espresso: "var(--kt-ai-fg)",
americano: "var(--kt-ai-fg-muted)",
latte: "var(--kt-ai-primary-subtle)",
mocha: "var(--kt-ai-border-strong)",
herb: "var(--kt-ai-chart-green)",
};
const MILK_COLORS = {
none: "transparent",
milk: "var(--kt-ai-bg)",
oat: "var(--kt-ai-bg-soft)",
almond: "var(--kt-ai-bg-hover)",
coconut: "var(--kt-ai-bg-elevated)",
};
const SYRUP_COLORS = {
none: "transparent",
vanilla: "var(--kt-ai-chip-orange-bg)",
caramel: "var(--kt-ai-chip-orange-fg)",
lavender: "var(--kt-ai-chip-purple-bg)",
honey: "var(--kt-ai-chip-orange-bg)",
};
const MOODS = {
wake: { base: "espresso", milk: "none", syrup: "none", size: "s", temp: "hot" },
soft: { base: "latte", milk: "milk", syrup: "vanilla", size: "m", temp: "hot" },
light: { base: "herb", milk: "none", syrup: "honey", size: "m", temp: "hot" },
};
const state = { base: null, milk: "none", syrup: "none", size: "m", temp: "hot" };
const $ = (id) => document.getElementById(id);
const byId = (group, id) => (PRODUCTS[group] || []).find((p) => p.id === id);
const fmt = (price) => price.toLocaleString("ru-RU") + " \u20B8";
function renderGroup(group) {
const wrap = $("group-" + group);
wrap.innerHTML = "";
PRODUCTS[group].forEach((opt) => {
const label = document.createElement("label");
label.className = "opt";
const input = document.createElement("input");
input.type = "radio";
input.name = "opt-" + group;
input.value = opt.id;
input.checked = state[group] === opt.id;
input.addEventListener("change", () => {
state[group] = opt.id;
update();
});
const name = document.createElement("span");
name.className = "opt-label";
name.textContent = opt.label;
label.appendChild(input);
label.appendChild(name);
if (opt.price > 0) {
const price = document.createElement("span");
price.className = "opt-price";
price.textContent = "+" + fmt(opt.price);
label.appendChild(price);
}
wrap.appendChild(label);
});
}
function selected(group) {
return byId(group, state[group]);
}
function priceTotal() {
if (!state.base) return 0;
const b = selected("base");
const m = selected("milk");
const s = selected("syrup");
const z = selected("size");
const t = selected("temp");
return (
b.price +
(b.hasMilk ? m.price : 0) +
(b.drink ? s.price : 0) +
z.price +
t.price
);
}
function paintCup() {
const layers = $("layers");
const ice = $("iceGroup");
const steam = $("steam");
layers.innerHTML = "";
ice.innerHTML = "";
if (!state.base) return;
const b = selected("base");
const m = selected("milk");
const s = selected("syrup");
const z = selected("size");
const t = selected("temp");
const fillFactor = z.id === "s" ? 0.45 : z.id === "m" ? 0.72 : 0.9;
const innerTop = 52;
const innerH = 158;
const fillH = Math.round(innerH * fillFactor);
const fillY = innerTop + (innerH - fillH);
const widthBySize = { s: 88, m: 124, l: 148 };
const width = widthBySize[z.id];
const cupX = 100 - width / 2;
[ $("cupInner"), $("cupBody") ].forEach((el) => {
el.setAttribute("width", width);
el.setAttribute("x", cupX);
});
const NS = "http://www.w3.org/2000/svg";
const addRect = (x, width, y, h, color, rx) => {
const rect = document.createElementNS(NS, "rect");
rect.setAttribute("x", x);
rect.setAttribute("width", width);
rect.setAttribute("y", Math.max(innerTop, y));
rect.setAttribute("height", h);
rect.setAttribute("fill", color);
rect.setAttribute("class", "part");
if (rx) rect.setAttribute("rx", rx);
layers.appendChild(rect);
return rect;
};
const innerX = cupX + 5;
const innerW = width - 10;
let y = fillY + fillH;
if (t.id === "iced") {
if (b.hasMilk && m.id !== "none") { y -= fillH * 0.35; addRect(innerX, innerW, y, fillH * 0.35, MILK_COLORS[m.id]); }
y -= fillH * 0.45; addRect(innerX, innerW, y, fillH * 0.45, BASE_COLORS[b.id]);
if (b.drink && s.id !== "none") { y -= fillH * 0.2; addRect(innerX, innerW, y, fillH * 0.2, SYRUP_COLORS[s.id]); }
for (let i = 0; i < 3; i++) {
const cube = document.createElementNS(NS, "rect");
cube.setAttribute("x", cupX + 10 + i * (innerW - 20) / 3);
cube.setAttribute("y", fillY + 6);
cube.setAttribute("width", Math.round((innerW - 20) / 3 - 8));
cube.setAttribute("height", "22");
cube.setAttribute("rx", "4");
cube.setAttribute("fill", "rgba(190,222,240,0.9)");
cube.setAttribute("stroke", "rgba(255,255,255,0.7)");
cube.setAttribute("stroke-width", "2");
cube.setAttribute("class", "part");
ice.appendChild(cube);
}
} else {
addRect(innerX, innerW, fillY, fillH * 0.45, BASE_COLORS[b.id]);
if (b.hasMilk && m.id !== "none") addRect(innerX, innerW, fillY + fillH * 0.45, fillH * 0.3, MILK_COLORS[m.id]);
if (b.drink && s.id !== "none") addRect(innerX, innerW, fillY + fillH * 0.75, fillH * 0.25, SYRUP_COLORS[s.id]);
}
steam.setAttribute("opacity", t.id === "hot" ? "1" : "0");
}
function buildName(b, m, s, z, t) {
const tempWord = t.id === "iced" ? "Ледяной" : "Горячий";
const sizeWord = z.id === "s" ? "маленький" : z.id === "m" ? "средний" : "большой";
const bits = [tempWord, sizeWord.toLowerCase()];
if (b.hasMilk && m.id !== "none") bits.push(m.label.toLowerCase());
if (b.drink && s.id !== "none") bits.push("сироп " + s.label.toLowerCase());
bits.push(b.label.toLowerCase());
return bits.join(", ");
}
function update() {
paintCup();
if (!state.base) return;
const b = selected("base");
const m = selected("milk");
const s = selected("syrup");
const z = selected("size");
const t = selected("temp");
$("buildName").textContent = buildName(b, m, s, z, t);
const lines = ["Основа: " + b.label, "Размер: " + z.label, "Подача: " + t.label];
if (b.hasMilk) lines.splice(1, 0, "Молоко: " + m.label);
if (b.drink) lines.splice(2, 0, "Сироп: " + s.label);
$("buildSummary").innerHTML = lines.map((l) => "<span>" + l + "</span>").join("");
const total = priceTotal();
const base = b.price;
$("buildPrice").innerHTML = "<small>Итого</small> " + fmt(total) +
(total > base ? " <small>(база " + fmt(base) + " + " + fmt(total - base) + ")</small>" : "");
}
function renderAll() {
Object.keys(PRODUCTS).forEach(renderGroup);
}
function applyMood(moodId) {
const preset = MOODS[moodId];
if (!preset) return;
Object.assign(state, preset);
renderAll();
update();
document.querySelectorAll(".mood-btn").forEach((btn) => {
const active = btn.dataset.mood === moodId;
btn.style.borderColor = active ? "var(--kt-ai-primary)" : "";
btn.style.background = active ? "var(--kt-ai-primary-subtle)" : "";
});
$("builderCard").hidden = false;
$("builderCard").scrollIntoView({ behavior: "smooth", block: "nearest" });
}
document.querySelectorAll(".mood-btn").forEach((btn) => {
btn.addEventListener("click", () => applyMood(btn.dataset.mood));
});
renderAll();