diff --git a/miniapp/assets/app.js b/miniapp/assets/app.js
index 528ddf0..43dec8f 100644
--- a/miniapp/assets/app.js
+++ b/miniapp/assets/app.js
@@ -1,24 +1,51 @@
-// ЗОВ MiniApp — главный скрипт
+// ЗОВ MiniApp — главный скрипт.
// На входе: подписанный initData от Telegram.
// Ходим на backend → получаем профиль (роль, статус) → рендерим меню.
const tg = window.Telegram?.WebApp;
-const BACKEND_URL = ""; // TODO: заполнить URL Apps Script Web App
+const BACKEND_URL = ""; // TODO: заполнить URL Apps Script Web App после Шага 4
const app = document.getElementById("app");
+/* ----------------- Telegram WebApp setup ----------------- */
+function setupTelegram() {
+ if (!tg) return;
+ try {
+ tg.ready();
+ tg.expand();
+ if (tg.setHeaderColor) tg.setHeaderColor("#003E7E");
+ if (tg.setBackgroundColor) tg.setBackgroundColor("#F4F4F5");
+ if (tg.enableClosingConfirmation) tg.enableClosingConfirmation();
+ } catch (e) { console.warn(e); }
+}
+
+function haptic(type = "selection") {
+ try {
+ if (!tg?.HapticFeedback) return;
+ if (type === "impact") tg.HapticFeedback.impactOccurred("light");
+ else if (type === "success") tg.HapticFeedback.notificationOccurred("success");
+ else tg.HapticFeedback.selectionChanged();
+ } catch (e) {}
+}
+
+/* ----------------- Data ----------------- */
async function fetchMe() {
if (!BACKEND_URL) {
- // dev-режим без backend — для локального просмотра вёрстки
+ // dev-режим без backend — мок для просмотра вёрстки
return {
role: "manager",
- user: { full_name: "Тест Менеджер", salon: "ЗОВ Москва" },
+ user: {
+ full_name: "Руслан Васильев",
+ salon: "ЗОВ Москва",
+ avatar_initial: "Р",
+ },
status: "active",
- status_until: "2026-08-12",
+ status_until: "12.08.2026",
};
}
const res = await fetch(`${BACKEND_URL}/api/me`, {
method: "POST",
+ headers: { "Content-Type": "application/json" },
body: JSON.stringify({
initData: tg?.initData || "",
startParam: tg?.initDataUnsafe?.start_param || null,
@@ -27,92 +54,172 @@ async function fetchMe() {
return res.json();
}
-function renderManager(me) {
- const status = me.status || "active";
- app.innerHTML = `
-
-
- `;
-}
-
-function renderClient(me) {
- app.innerHTML = `
-
-
- `;
+/* ----------------- Helpers ----------------- */
+function el(html) {
+ const t = document.createElement("template");
+ t.innerHTML = html.trim();
+ return t.content.firstChild;
}
function statusLabel(s) {
- return { active: "🟢 active", lapsed: "🔴 lapsed", grace: "🟡 grace" }[s] || s;
+ return ({
+ active: "Доступ открыт",
+ lapsed: "Доступ ограничен",
+ grace: "Грейс-период",
+ })[s] || s;
}
+function getInitial(name) {
+ return (name || "").trim().slice(0, 1).toUpperCase() || "?";
+}
+
+/* ----------------- Renders ----------------- */
+function renderManager(me) {
+ const status = me.status || "active";
+ const statusUntil = me.status_until ? `до ${me.status_until}` : "";
+ const initial = me.user?.avatar_initial || getInitial(me.user?.full_name);
+
+ app.innerHTML = "";
+
+ app.appendChild(el(`
+
+ `));
+
+ const sections = [
+ {
+ label: "Работа с клиентами",
+ items: [
+ { icon: "wrench", color: "green", label: "Подбор техники для клиента", href: "#/m/podbor" },
+ { icon: "ruler", color: "blue", label: "Замеры", href: "#/m/measurements" },
+ { icon: "clipboard", color: "gold", label: "Заявки клиентов", soon: true },
+ { icon: "briefcase", color: "gray", label: "Сделки", soon: true },
+ ],
+ },
+ {
+ label: "Аккаунт",
+ items: [
+ { icon: "wallet", color: "gold", label: "Мой статус и доступ", href: "#/m/status" },
+ { icon: "help", color: "blue", label: "Связь с куратором", href: "#/m/help" },
+ ],
+ },
+ ];
+
+ sections.forEach(section => {
+ app.appendChild(el(`${section.label}
`));
+ app.appendChild(buildMenu(section.items));
+ });
+
+ app.appendChild(el(`
+
+ `));
+}
+
+function renderClient(me) {
+ const initial = me.user?.avatar_initial || getInitial(me.user?.full_name) || "?";
+ const greetName = me.user?.full_name || "Здравствуйте";
+
+ app.innerHTML = "";
+
+ app.appendChild(el(`
+
+ `));
+
+ const sections = [
+ {
+ label: "Подобрать кухню",
+ items: [
+ { icon: "ruler", color: "blue", label: "Замер кухни", href: "#/c/measure" },
+ { icon: "wrench", color: "green", label: "Подобрать технику", soon: true },
+ { icon: "wallet", color: "gold", label: "Калькулятор бюджета", soon: true },
+ ],
+ },
+ {
+ label: "Помощь",
+ items: [
+ { icon: "lightbulb", color: "gold", label: "Идеи и кейсы", soon: true },
+ { icon: "phone", color: "blue", label: "Связаться с менеджером", href: "#/c/contact" },
+ { icon: "pin", color: "green", label: "Записаться в салон", soon: true },
+ ],
+ },
+ ];
+
+ sections.forEach(section => {
+ app.appendChild(el(`${section.label}
`));
+ app.appendChild(buildMenu(section.items));
+ });
+
+ app.appendChild(el(`
+
+ `));
+}
+
+function buildMenu(items) {
+ const menu = el(``);
+ items.forEach(item => {
+ const cls = item.soon ? "menu-item disabled" : "menu-item";
+ const node = el(`
+
+ ${ICONS[item.icon] || ""}
+
+
+ ${item.label}
+ ${item.soon ? 'скоро' : ""}
+
+ ${item.sub ? `
${item.sub}
` : ""}
+
+ ${item.soon ? "" : `${ICONS.chevron}
`}
+
+ `);
+ if (!item.soon) node.addEventListener("click", () => haptic("impact"));
+ menu.appendChild(node);
+ });
+ return menu;
+}
+
+function renderError() {
+ app.innerHTML = "";
+ app.appendChild(el(`
+
+
Не удалось загрузить кабинет
+
Проверьте подключение и попробуйте позже.
+
+ `));
+}
+
+/* ----------------- Init ----------------- */
async function init() {
- if (tg) {
- tg.ready();
- tg.expand();
- }
+ setupTelegram();
try {
const me = await fetchMe();
if (me.role === "manager") renderManager(me);
else renderClient(me);
} catch (e) {
- app.innerHTML = `Ошибка загрузки. Попробуйте позже.
`;
console.error(e);
+ renderError();
}
}
diff --git a/miniapp/assets/icons.js b/miniapp/assets/icons.js
new file mode 100644
index 0000000..2503e8a
--- /dev/null
+++ b/miniapp/assets/icons.js
@@ -0,0 +1,30 @@
+// Lucide-style line icons (24x24, stroke 2). Используем currentColor.
+// MIT-аналоги, перерисованы вручную для облегчения веса.
+
+const ICONS = {
+ wrench: ``,
+
+ ruler: ``,
+
+ clipboard: ``,
+
+ briefcase: ``,
+
+ wallet: ``,
+
+ phone: ``,
+
+ home: ``,
+
+ lightbulb: ``,
+
+ pin: ``,
+
+ calendar: ``,
+
+ help: ``,
+
+ user: ``,
+
+ chevron: ``,
+};
diff --git a/miniapp/assets/styles.css b/miniapp/assets/styles.css
index 8a990ba..0ac6d8e 100644
--- a/miniapp/assets/styles.css
+++ b/miniapp/assets/styles.css
@@ -1,107 +1,398 @@
+/* ============================================================
+ ЗОВ MiniApp — design system v2
+ ============================================================ */
+
:root {
- --bronze: #76BD22;
- --accent: #003E7E;
- --cream: #FAFAFA;
- --sand: #F0F9E8;
- --ink: #1B1B1B;
- --muted: #6B6B6B;
- --line: #E5E5E5;
+ /* Brand */
+ --brand-green: #76BD22;
+ --brand-green-dark: #5FA316;
+ --brand-blue: #003E7E;
+ --brand-blue-dark: #002952;
+ --brand-cream: #FAF8F3;
+ --brand-gold: #C9A95E;
- --r: 12px;
- --shadow: 0 1px 3px rgba(0, 0, 0, 0.06);
+ /* Neutral palette adapts to Telegram theme */
+ --bg: var(--tg-theme-bg-color, #FFFFFF);
+ --bg-secondary: var(--tg-theme-secondary-bg-color, #F4F4F5);
+ --bg-section: var(--tg-theme-section-bg-color, #FFFFFF);
+ --text: var(--tg-theme-text-color, #0F0F0F);
+ --text-muted: var(--tg-theme-hint-color, #707579);
+ --text-section: var(--tg-theme-section-header-text-color, #707579);
+ --link: var(--tg-theme-link-color, #003E7E);
+ --line: rgba(0, 0, 0, 0.08);
+
+ /* Status */
+ --status-active: #76BD22;
+ --status-active-bg: #EEF7E0;
+ --status-lapsed: #DC2626;
+ --status-lapsed-bg: #FEE2E2;
+ --status-grace: #D97706;
+ --status-grace-bg: #FEF3C7;
+
+ /* Shadows */
+ --shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.04);
+ --shadow-md: 0 4px 16px rgba(0, 0, 0, 0.06);
+ --shadow-lg: 0 8px 32px rgba(0, 62, 126, 0.18);
+
+ /* Radii */
+ --r-sm: 8px;
+ --r-md: 12px;
+ --r-lg: 16px;
+ --r-xl: 20px;
+
+ /* Spacing */
+ --s1: 4px; --s2: 8px; --s3: 12px; --s4: 16px;
+ --s5: 20px; --s6: 24px; --s7: 28px; --s8: 32px;
+
+ /* Type */
+ --font-body: -apple-system, BlinkMacSystemFont, "SF Pro Text", "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
}
+/* Dark theme overrides */
+@media (prefers-color-scheme: dark) {
+ :root {
+ --line: rgba(255, 255, 255, 0.10);
+ --status-active-bg: rgba(118, 189, 34, 0.15);
+ --status-lapsed-bg: rgba(220, 38, 38, 0.15);
+ --status-grace-bg: rgba(217, 119, 6, 0.15);
+ --shadow-md: 0 4px 16px rgba(0, 0, 0, 0.3);
+ }
+}
+
+/* ============================================================
+ Reset
+ ============================================================ */
* { box-sizing: border-box; }
+* { -webkit-tap-highlight-color: transparent; }
+html, body { margin: 0; padding: 0; }
-html, body {
- margin: 0;
- padding: 0;
- font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
- color: var(--ink);
- background: var(--cream);
+body {
+ font-family: var(--font-body);
+ font-size: 16px;
+ line-height: 1.4;
+ color: var(--text);
+ background: var(--bg-secondary);
+ -webkit-font-smoothing: antialiased;
+ -moz-osx-font-smoothing: grayscale;
+ text-rendering: optimizeLegibility;
}
+a { color: inherit; text-decoration: none; }
+button { font: inherit; cursor: pointer; border: none; background: none; color: inherit; }
+
+/* ============================================================
+ Layout
+ ============================================================ */
#app {
- max-width: 720px;
+ max-width: 560px;
margin: 0 auto;
- padding: 16px;
+ padding: var(--s4);
+ padding-bottom: calc(var(--s8) + env(safe-area-inset-bottom));
min-height: 100vh;
}
+/* ============================================================
+ Loader
+ ============================================================ */
.loader {
- text-align: center;
- padding: 64px 16px;
- color: var(--muted);
-}
-
-.header {
- margin-bottom: 16px;
-}
-
-.header h1 {
- font-size: 20px;
- margin: 0 0 4px;
- color: var(--accent);
-}
-
-.header .subtitle {
- font-size: 14px;
- color: var(--muted);
-}
-
-.menu {
display: grid;
- gap: 8px;
+ place-items: center;
+ min-height: 60vh;
}
-.menu-item {
+.spinner {
+ width: 36px;
+ height: 36px;
+ border: 3px solid var(--line);
+ border-top-color: var(--brand-blue);
+ border-radius: 50%;
+ animation: spin 0.7s linear infinite;
+}
+
+@keyframes spin {
+ to { transform: rotate(360deg); }
+}
+
+/* ============================================================
+ Profile card (header)
+ ============================================================ */
+.profile-card {
+ background: linear-gradient(135deg, var(--brand-blue) 0%, var(--brand-blue-dark) 100%);
+ color: #FFFFFF;
+ border-radius: var(--r-lg);
+ padding: var(--s5);
+ margin-bottom: var(--s5);
+ box-shadow: var(--shadow-lg);
display: flex;
align-items: center;
- gap: 12px;
- padding: 16px;
- background: white;
- border: 1px solid var(--line);
- border-radius: var(--r);
- cursor: pointer;
- user-select: none;
- text-decoration: none;
- color: inherit;
- transition: transform 0.05s, box-shadow 0.1s;
+ gap: var(--s4);
+ position: relative;
+ overflow: hidden;
}
-.menu-item:active {
- transform: scale(0.99);
- box-shadow: var(--shadow);
+.profile-card::before {
+ content: "";
+ position: absolute;
+ right: -40px;
+ top: -40px;
+ width: 160px;
+ height: 160px;
+ background: var(--brand-green);
+ opacity: 0.18;
+ border-radius: 50%;
+ filter: blur(8px);
}
-.menu-item.disabled {
- opacity: 0.4;
- cursor: not-allowed;
+.profile-card::after {
+ content: "";
+ position: absolute;
+ right: 16px;
+ bottom: -20px;
+ width: 80px;
+ height: 80px;
+ background: var(--brand-gold);
+ opacity: 0.12;
+ border-radius: 50%;
}
-.menu-item .icon {
- font-size: 22px;
+.profile-card .avatar {
+ width: 56px;
+ height: 56px;
+ border-radius: 50%;
+ background: rgba(255, 255, 255, 0.15);
+ backdrop-filter: blur(8px);
+ display: grid;
+ place-items: center;
flex-shrink: 0;
+ font-size: 24px;
+ font-weight: 600;
+ color: #FFFFFF;
+ position: relative;
+ z-index: 1;
}
-.menu-item .label {
+.profile-card .info {
flex: 1;
- font-weight: 500;
+ min-width: 0;
+ position: relative;
+ z-index: 1;
}
-.menu-item .arrow {
- color: var(--muted);
- font-size: 18px;
+.profile-card .role-tag {
+ font-size: 11px;
+ font-weight: 600;
+ text-transform: uppercase;
+ letter-spacing: 0.08em;
+ opacity: 0.7;
+ margin-bottom: 2px;
}
-.status-badge {
- display: inline-block;
- padding: 2px 8px;
+.profile-card .name {
+ font-size: 19px;
+ font-weight: 600;
+ letter-spacing: -0.01em;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+}
+
+.profile-card .meta {
+ font-size: 13px;
+ opacity: 0.85;
+ margin-top: 2px;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+}
+
+.profile-card .status-row {
+ display: inline-flex;
+ align-items: center;
+ gap: 6px;
+ margin-top: var(--s2);
+ padding: 4px 10px 4px 6px;
+ background: rgba(255, 255, 255, 0.15);
+ backdrop-filter: blur(8px);
border-radius: 999px;
font-size: 12px;
font-weight: 500;
}
-.status-badge.active { background: var(--sand); color: var(--bronze); }
-.status-badge.lapsed { background: #FFF1F0; color: #C82C2C; }
-.status-badge.grace { background: #FFF8E1; color: #B07E00; }
+.profile-card .status-dot {
+ width: 8px;
+ height: 8px;
+ border-radius: 50%;
+ background: var(--brand-green);
+ box-shadow: 0 0 0 3px rgba(118, 189, 34, 0.4);
+ animation: pulse 2s ease-in-out infinite;
+}
+
+@keyframes pulse {
+ 0%, 100% { box-shadow: 0 0 0 3px rgba(118, 189, 34, 0.4); }
+ 50% { box-shadow: 0 0 0 6px rgba(118, 189, 34, 0.0); }
+}
+
+.profile-card .status-dot.lapsed { background: #FCA5A5; box-shadow: 0 0 0 3px rgba(252, 165, 165, 0.4); animation: none; }
+.profile-card .status-dot.grace { background: #FCD34D; box-shadow: 0 0 0 3px rgba(252, 211, 77, 0.4); }
+
+/* ============================================================
+ Section label
+ ============================================================ */
+.section-label {
+ font-size: 12px;
+ font-weight: 600;
+ text-transform: uppercase;
+ letter-spacing: 0.06em;
+ color: var(--text-section);
+ padding: 0 var(--s4) var(--s2);
+ margin-top: var(--s5);
+}
+
+.section-label:first-child { margin-top: 0; }
+
+/* ============================================================
+ Menu (grouped list)
+ ============================================================ */
+.menu {
+ background: var(--bg-section);
+ border-radius: var(--r-md);
+ overflow: hidden;
+ box-shadow: var(--shadow-sm);
+}
+
+.menu + .menu { margin-top: var(--s4); }
+
+.menu-item {
+ display: flex;
+ align-items: center;
+ gap: var(--s3);
+ padding: var(--s3) var(--s4);
+ min-height: 64px;
+ color: var(--text);
+ cursor: pointer;
+ transition: background 0.15s;
+ position: relative;
+}
+
+.menu-item + .menu-item::before {
+ content: "";
+ position: absolute;
+ top: 0;
+ left: 64px;
+ right: 0;
+ height: 1px;
+ background: var(--line);
+}
+
+.menu-item:active {
+ background: var(--bg-secondary);
+}
+
+.menu-item .icon {
+ width: 36px;
+ height: 36px;
+ border-radius: 9px;
+ display: grid;
+ place-items: center;
+ flex-shrink: 0;
+}
+
+.menu-item .icon svg {
+ width: 20px;
+ height: 20px;
+ stroke-width: 2;
+}
+
+/* Icon variants */
+.menu-item .icon.green { background: #EEF7E0; color: #5FA316; }
+.menu-item .icon.blue { background: #E5EDF5; color: #003E7E; }
+.menu-item .icon.gold { background: #FAF1DC; color: #BF8A2E; }
+.menu-item .icon.gray { background: var(--bg-secondary); color: var(--text-muted); }
+.menu-item .icon.red { background: #FEE2E2; color: #DC2626; }
+
+@media (prefers-color-scheme: dark) {
+ .menu-item .icon.green { background: rgba(118, 189, 34, 0.15); color: #A4D85F; }
+ .menu-item .icon.blue { background: rgba(0, 62, 126, 0.25); color: #6FA0D6; }
+ .menu-item .icon.gold { background: rgba(201, 169, 94, 0.15); color: #E0C079; }
+ .menu-item .icon.red { background: rgba(220, 38, 38, 0.15); color: #F87171; }
+}
+
+.menu-item .text {
+ flex: 1;
+ min-width: 0;
+}
+
+.menu-item .label {
+ font-size: 15.5px;
+ font-weight: 500;
+ letter-spacing: -0.01em;
+ display: flex;
+ align-items: center;
+ gap: var(--s2);
+}
+
+.menu-item .sub {
+ font-size: 13px;
+ color: var(--text-muted);
+ margin-top: 2px;
+}
+
+.menu-item .chevron {
+ color: var(--text-muted);
+ flex-shrink: 0;
+ opacity: 0.5;
+}
+
+.menu-item.disabled {
+ cursor: not-allowed;
+}
+
+.menu-item.disabled .label,
+.menu-item.disabled .icon {
+ opacity: 0.45;
+}
+
+.badge {
+ display: inline-block;
+ font-size: 10px;
+ font-weight: 700;
+ text-transform: uppercase;
+ letter-spacing: 0.06em;
+ padding: 2px 7px;
+ border-radius: 999px;
+ background: var(--bg-secondary);
+ color: var(--text-muted);
+ vertical-align: 1px;
+}
+
+/* ============================================================
+ Footer hint
+ ============================================================ */
+.footer-hint {
+ text-align: center;
+ font-size: 12px;
+ color: var(--text-muted);
+ margin-top: var(--s6);
+ padding: 0 var(--s4);
+ line-height: 1.5;
+}
+
+.footer-hint a { color: var(--link); }
+
+/* ============================================================
+ Error state
+ ============================================================ */
+.error {
+ background: var(--bg-section);
+ border-radius: var(--r-md);
+ padding: var(--s5);
+ text-align: center;
+ color: var(--text-muted);
+ box-shadow: var(--shadow-sm);
+}
+
+.error h3 {
+ margin: 0 0 var(--s2);
+ color: var(--text);
+ font-size: 17px;
+}
diff --git a/miniapp/index.html b/miniapp/index.html
index 16fdbd3..e5b5b4e 100644
--- a/miniapp/index.html
+++ b/miniapp/index.html
@@ -2,15 +2,19 @@
-
+
+
ЗОВ — Кабинет
- Загрузка...
+
+