mirror of
https://github.com/wasrusgen/zov-tech.git
synced 2026-06-03 15:04:50 +00:00
feat(miniapp): premium redesign — gradient profile card, SVG icons, native-style grouped menus, dark theme
This commit is contained in:
parent
6b0b01e15e
commit
57eefbbf5c
@ -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 = `
|
||||
<div class="header">
|
||||
<h1>Кабинет менеджера</h1>
|
||||
<div class="subtitle">
|
||||
${me.user.full_name} · ${me.user.salon || ""}
|
||||
<span class="status-badge ${status}">${statusLabel(status)}</span>
|
||||
</div>
|
||||
</div>
|
||||
<nav class="menu">
|
||||
<a class="menu-item" href="#/m/podbor">
|
||||
<span class="icon">🔧</span>
|
||||
<span class="label">Подбор техники для клиента</span>
|
||||
<span class="arrow">›</span>
|
||||
</a>
|
||||
<a class="menu-item" href="#/m/measurements">
|
||||
<span class="icon">📐</span>
|
||||
<span class="label">Замеры</span>
|
||||
<span class="arrow">›</span>
|
||||
</a>
|
||||
<div class="menu-item disabled">
|
||||
<span class="icon">📋</span>
|
||||
<span class="label">Заявки клиентов <small>(скоро)</small></span>
|
||||
</div>
|
||||
<div class="menu-item disabled">
|
||||
<span class="icon">💼</span>
|
||||
<span class="label">Сделки <small>(скоро)</small></span>
|
||||
</div>
|
||||
<a class="menu-item" href="#/m/status">
|
||||
<span class="icon">💰</span>
|
||||
<span class="label">Мой статус и доступ</span>
|
||||
<span class="arrow">›</span>
|
||||
</a>
|
||||
</nav>
|
||||
`;
|
||||
}
|
||||
|
||||
function renderClient(me) {
|
||||
app.innerHTML = `
|
||||
<div class="header">
|
||||
<h1>Кабинет клиента</h1>
|
||||
<div class="subtitle">
|
||||
${me.user.full_name || "Здравствуйте!"}
|
||||
${me.manager ? `<br>Менеджер: ${me.manager.full_name}, ${me.manager.salon || ""}` : ""}
|
||||
</div>
|
||||
</div>
|
||||
<nav class="menu">
|
||||
<a class="menu-item" href="#/c/measure">
|
||||
<span class="icon">📐</span>
|
||||
<span class="label">Замер кухни</span>
|
||||
<span class="arrow">›</span>
|
||||
</a>
|
||||
<div class="menu-item disabled">
|
||||
<span class="icon">🔧</span>
|
||||
<span class="label">Подобрать технику <small>(скоро)</small></span>
|
||||
</div>
|
||||
<div class="menu-item disabled">
|
||||
<span class="icon">💡</span>
|
||||
<span class="label">Идеи и кейсы <small>(скоро)</small></span>
|
||||
</div>
|
||||
<a class="menu-item" href="#/c/contact">
|
||||
<span class="icon">📞</span>
|
||||
<span class="label">Связаться с менеджером</span>
|
||||
<span class="arrow">›</span>
|
||||
</a>
|
||||
</nav>
|
||||
`;
|
||||
/* ----------------- 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(`
|
||||
<header class="profile-card">
|
||||
<div class="avatar">${initial}</div>
|
||||
<div class="info">
|
||||
<div class="role-tag">Менеджер</div>
|
||||
<div class="name">${me.user?.full_name || ""}</div>
|
||||
<div class="meta">${me.user?.salon || ""}</div>
|
||||
<span class="status-row">
|
||||
<span class="status-dot ${status}"></span>
|
||||
<span>${statusLabel(status)}${statusUntil ? " · " + statusUntil : ""}</span>
|
||||
</span>
|
||||
</div>
|
||||
</header>
|
||||
`));
|
||||
|
||||
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(`<div class="section-label">${section.label}</div>`));
|
||||
app.appendChild(buildMenu(section.items));
|
||||
});
|
||||
|
||||
app.appendChild(el(`
|
||||
<div class="footer-hint">
|
||||
Куратор партнёрской сети — Руслан Васильев<br>
|
||||
<a href="https://t.me/wasrusgen">@wasrusgen</a>
|
||||
</div>
|
||||
`));
|
||||
}
|
||||
|
||||
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(`
|
||||
<header class="profile-card">
|
||||
<div class="avatar">${initial}</div>
|
||||
<div class="info">
|
||||
<div class="role-tag">Клиент</div>
|
||||
<div class="name">${greetName}</div>
|
||||
<div class="meta">${me.manager ? "Менеджер: " + me.manager.full_name + (me.manager.salon ? ", " + me.manager.salon : "") : "ЗОВ — кухонная мебель"}</div>
|
||||
</div>
|
||||
</header>
|
||||
`));
|
||||
|
||||
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(`<div class="section-label">${section.label}</div>`));
|
||||
app.appendChild(buildMenu(section.items));
|
||||
});
|
||||
|
||||
app.appendChild(el(`
|
||||
<div class="footer-hint">
|
||||
Фабрика кухонной мебели <strong>ЗОВ</strong><br>
|
||||
<a href="https://t.me/wasrusgen1">канал @wasrusgen1</a>
|
||||
</div>
|
||||
`));
|
||||
}
|
||||
|
||||
function buildMenu(items) {
|
||||
const menu = el(`<nav class="menu"></nav>`);
|
||||
items.forEach(item => {
|
||||
const cls = item.soon ? "menu-item disabled" : "menu-item";
|
||||
const node = el(`
|
||||
<a class="${cls}" ${item.href && !item.soon ? `href="${item.href}"` : ""}>
|
||||
<div class="icon ${item.color}">${ICONS[item.icon] || ""}</div>
|
||||
<div class="text">
|
||||
<div class="label">
|
||||
${item.label}
|
||||
${item.soon ? '<span class="badge">скоро</span>' : ""}
|
||||
</div>
|
||||
${item.sub ? `<div class="sub">${item.sub}</div>` : ""}
|
||||
</div>
|
||||
${item.soon ? "" : `<div class="chevron">${ICONS.chevron}</div>`}
|
||||
</a>
|
||||
`);
|
||||
if (!item.soon) node.addEventListener("click", () => haptic("impact"));
|
||||
menu.appendChild(node);
|
||||
});
|
||||
return menu;
|
||||
}
|
||||
|
||||
function renderError() {
|
||||
app.innerHTML = "";
|
||||
app.appendChild(el(`
|
||||
<div class="error">
|
||||
<h3>Не удалось загрузить кабинет</h3>
|
||||
<div>Проверьте подключение и попробуйте позже.</div>
|
||||
</div>
|
||||
`));
|
||||
}
|
||||
|
||||
/* ----------------- 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 = `<div class="loader">Ошибка загрузки. Попробуйте позже.</div>`;
|
||||
console.error(e);
|
||||
renderError();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
30
miniapp/assets/icons.js
Normal file
30
miniapp/assets/icons.js
Normal file
@ -0,0 +1,30 @@
|
||||
// Lucide-style line icons (24x24, stroke 2). Используем currentColor.
|
||||
// MIT-аналоги, перерисованы вручную для облегчения веса.
|
||||
|
||||
const ICONS = {
|
||||
wrench: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"><path d="M14.7 6.3a1 1 0 0 0 0 1.4l1.6 1.6a1 1 0 0 0 1.4 0l3.77-3.77a6 6 0 0 1-7.94 7.94l-6.91 6.91a2.12 2.12 0 0 1-3-3l6.91-6.91a6 6 0 0 1 7.94-7.94l-3.76 3.76z"/></svg>`,
|
||||
|
||||
ruler: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"><path d="M21.3 15.3a2.4 2.4 0 0 1 0 3.4l-2.6 2.6a2.4 2.4 0 0 1-3.4 0L2.7 8.7a2.4 2.4 0 0 1 0-3.4l2.6-2.6a2.4 2.4 0 0 1 3.4 0Z"/><path d="M14.5 12.5 12 15"/><path d="M11.5 9.5 9 12"/><path d="M8.5 6.5 6 9"/><path d="M17.5 15.5 15 18"/></svg>`,
|
||||
|
||||
clipboard: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"><rect width="8" height="4" x="8" y="2" rx="1" ry="1"/><path d="M16 4h2a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h2"/></svg>`,
|
||||
|
||||
briefcase: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"><path d="M16 20V4a2 2 0 0 0-2-2h-4a2 2 0 0 0-2 2v16"/><rect width="20" height="14" x="2" y="6" rx="2"/></svg>`,
|
||||
|
||||
wallet: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"><path d="M19 7V4a1 1 0 0 0-1-1H5a2 2 0 0 0 0 4h15a1 1 0 0 1 1 1v4"/><path d="M3 5v14a2 2 0 0 0 2 2h15a1 1 0 0 0 1-1v-4"/><path d="M18 12a2 2 0 0 0 0 4h4v-4Z"/></svg>`,
|
||||
|
||||
phone: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"><path d="M22 16.92v3a2 2 0 0 1-2.18 2 19.79 19.79 0 0 1-8.63-3.07 19.5 19.5 0 0 1-6-6 19.79 19.79 0 0 1-3.07-8.67A2 2 0 0 1 4.11 2h3a2 2 0 0 1 2 1.72 12.84 12.84 0 0 0 .7 2.81 2 2 0 0 1-.45 2.11L8.09 9.91a16 16 0 0 0 6 6l1.27-1.27a2 2 0 0 1 2.11-.45 12.84 12.84 0 0 0 2.81.7A2 2 0 0 1 22 16.92z"/></svg>`,
|
||||
|
||||
home: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"><path d="M15 21v-8a1 1 0 0 0-1-1h-4a1 1 0 0 0-1 1v8"/><path d="M3 10a2 2 0 0 1 .709-1.528l7-5.999a2 2 0 0 1 2.582 0l7 5.999A2 2 0 0 1 21 10v9a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/></svg>`,
|
||||
|
||||
lightbulb: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"><path d="M15 14c.2-1 .7-1.7 1.5-2.5 1-.9 1.5-2.2 1.5-3.5A6 6 0 0 0 6 8c0 1 .2 2.2 1.5 3.5.7.7 1.3 1.5 1.5 2.5"/><path d="M9 18h6"/><path d="M10 22h4"/></svg>`,
|
||||
|
||||
pin: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"><path d="M20 10c0 4.993-5.539 10.193-7.399 11.799a1 1 0 0 1-1.202 0C9.539 20.193 4 14.993 4 10a8 8 0 0 1 16 0"/><circle cx="12" cy="10" r="3"/></svg>`,
|
||||
|
||||
calendar: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"><path d="M8 2v4"/><path d="M16 2v4"/><rect width="18" height="18" x="3" y="4" rx="2"/><path d="M3 10h18"/></svg>`,
|
||||
|
||||
help: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3"/><path d="M12 17h.01"/></svg>`,
|
||||
|
||||
user: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2.2"><circle cx="12" cy="8" r="4"/><path d="M4 21a8 8 0 0 1 16 0"/></svg>`,
|
||||
|
||||
chevron: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2.2"><path d="m9 6 6 6-6 6"/></svg>`,
|
||||
};
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -2,15 +2,19 @@
|
||||
<html lang="ru">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover, user-scalable=no">
|
||||
<meta name="theme-color" content="#003E7E">
|
||||
<title>ЗОВ — Кабинет</title>
|
||||
<script src="https://telegram.org/js/telegram-web-app.js"></script>
|
||||
<link rel="stylesheet" href="assets/styles.css">
|
||||
</head>
|
||||
<body>
|
||||
<main id="app">
|
||||
<div class="loader">Загрузка...</div>
|
||||
<div class="loader">
|
||||
<div class="spinner"></div>
|
||||
</div>
|
||||
</main>
|
||||
<script src="assets/icons.js"></script>
|
||||
<script src="assets/app.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
Loading…
Reference in New Issue
Block a user