сборщик кофе: настроение, основа/молоко/сироп/размер/поподача, живой стикер и цена
This commit is contained in:
parent
4d61a313ad
commit
91571b139f
189
index.html
189
index.html
@ -10,13 +10,14 @@
|
||||
<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/vibe-theme.css">
|
||||
<script src="design-system/icons/kt-ai-icons.js"></script>
|
||||
<style>
|
||||
a.kt-ai-btn { text-decoration: none; }
|
||||
|
||||
.menu-photo {
|
||||
position: relative;
|
||||
margin-top: var(--kt-ai-space-6);
|
||||
border-radius: var(--kt-ai-radius-lg, 12px);
|
||||
border-radius: var(--kt-ai-radius-lg);
|
||||
overflow: hidden;
|
||||
border: 1px solid var(--kt-ai-border);
|
||||
background: var(--kt-ai-bg-soft);
|
||||
@ -64,6 +65,122 @@
|
||||
gap: var(--kt-ai-space-3);
|
||||
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>
|
||||
</head>
|
||||
<body>
|
||||
@ -74,6 +191,7 @@
|
||||
<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="#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="primary" href="#contacts">Как нас найти</a>
|
||||
</div>
|
||||
@ -84,8 +202,72 @@
|
||||
<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>
|
||||
<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-size="lg" href="#contacts">Как нас найти</a>
|
||||
<a class="kt-ai-btn" data-variant="primary" data-size="lg" href="#builder">Собрать свой кофе</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>
|
||||
</section>
|
||||
@ -226,5 +408,6 @@
|
||||
</span>
|
||||
</footer>
|
||||
|
||||
<script src="script.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
247
script.js
Normal file
247
script.js
Normal 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();
|
||||
Loading…
Reference in New Issue
Block a user