Оформление стилями CRM (tvorymo-ui.css + crm.css) и демо компонентов

This commit is contained in:
crm-embed-test
2026-10-13 12:45:30 +00:00
parent a92286d413
commit caae1924f8

View File

@@ -4,49 +4,189 @@
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>CRM embed test</title>
<style>
body { font: 15px/1.5 system-ui, sans-serif; margin: 16px; color: #222; }
h1 { font-size: 18px; margin: 0 0 12px; }
#status { padding: 8px 12px; border-radius: 6px; background: #fff4d6; }
#status.ok { background: #e3f6e3; } #status.err { background: #fde2e2; }
dl { display: grid; grid-template-columns: max-content 1fr; gap: 4px 12px; }
dt { color: #666; } dd { margin: 0; word-break: break-all; }
button { margin-top: 12px; padding: 6px 14px; }
#log { margin-top: 16px; font-size: 13px; color: #555; padding-left: 18px; }
</style>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<!-- Стили CRM Tvorymo: сначала дизайн-система, затем стили самой CRM (так требует шапка crm.css). -->
<link rel="stylesheet" href="https://test.units.tvorymo.com/tvorymo-crm/css/tvorymo-ui.css">
<link rel="stylesheet" href="https://test.units.tvorymo.com/tvorymo-crm/css/crm.css">
</head>
<body>
<h1>Тест встраивания в CRM Tvorymo</h1>
<div id="status">жду авторизацию от CRM</div>
<dl id="info" hidden>
<dt>Имя</dt><dd id="name"></dd>
<dt>Email</dt><dd id="email"></dd>
<dt>ID</dt><dd id="uid"></dd>
<dt>Origin отправителя</dt><dd id="origin"></dd>
<dt>expires_at</dt><dd id="expires"></dd>
<dt>Получено токенов</dt><dd id="count">0</dd>
</dl>
<button id="refresh" hidden>Обновить токен</button>
<ol id="log"></ol>
<body class="side-off">
<main id="main">
<div class="page">
<div class="head">
<div>
<h1>Тест встраивания в&nbsp;CRM</h1>
<div class="sub">Оформлено стилями CRM: <b>tvorymo-ui.css</b> + <b>crm.css</b></div>
</div>
<span class="chip"><span class="syncdot off" id="dot"></span><span id="mode">—</span></span>
</div>
<div class="wsinfo">
<svg class="ic" viewBox="0 0 24 24"><circle cx="12" cy="12" r="9"/><path d="M12 8v5M12 16h.01"/></svg>
<div class="grow">
<span id="status" class="chip gold">жду авторизацию от CRM</span>
<div class="small mt-6">Страница получает сессию CRM через <span class="code">postMessage</span> и проверяет токен в Supabase на сервере.</div>
</div>
</div>
<div class="sec"><h2>Пользователь CRM <b id="count">0</b></h2><span class="small mute">токенов получено</span></div>
<div id="info" hidden>
<div class="pcard">
<div class="t1">
<span class="mono s44" id="initials">?</span>
<div class="min-w-0">
<div class="ttl" id="name"></div>
<div class="ds" id="email"></div>
</div>
<div class="n"><b id="mins">—</b><small>мин до exp</small></div>
</div>
<div class="mt">
<span class="chip fill">Supabase ✓</span>
<span class="chip gold">aud: authenticated</span>
<span class="chip" id="changed">токен первый</span>
</div>
<div>
<div class="mrow"><div class="t">ID<small class="code" id="uid"></small></div></div>
<div class="mrow"><div class="t">Origin отправителя<small id="origin"></small></div></div>
<div class="mrow"><div class="t">expires_at<small id="expires"></small></div></div>
</div>
<div class="row wrap">
<button class="btn gold" id="refresh" hidden>
<svg class="ic" viewBox="0 0 24 24"><path d="M20 11a8 8 0 1 0-2.3 5.7M20 4v7h-7"/></svg>
Обновить токен
</button>
</div>
</div>
</div>
<div class="empty" id="noinfo">Пока нет данных. Откройте страницу из боковой панели CRM — она пришлёт <b>crm:auth</b>.</div>
<div class="sec"><h2>Журнал</h2></div>
<div id="log"><div class="empty">событий нет</div></div>
<hr>
<div class="sec"><h2>Демо компонентов CRM</h2><span class="small mute">не функциональны</span></div>
<div class="stat">
<div><b>12</b><small>задач</small></div>
<div><b class="gold">5</b><small>в работе</small></div>
<div><b class="danger">2</b><small>просрочено</small></div>
<div><b class="ok">7</b><small>готово</small></div>
</div>
<div class="f"><label>Кнопки</label></div>
<div class="row wrap mt-6">
<button class="btn gold">Главная</button>
<button class="btn">Обычная</button>
<button class="btn ghost">Ghost</button>
<button class="btn dng">Удалить</button>
<button class="btn sm">Маленькая</button>
<button class="btn" disabled>Disabled</button>
<button class="icon-btn" title="Настройки"><svg class="ic" viewBox="0 0 24 24"><circle cx="12" cy="12" r="3"/><path d="M19 12h2M3 12h2M12 3v2M12 19v2M5.6 5.6l1.4 1.4M17 17l1.4 1.4M5.6 18.4 7 17M17 7l1.4-1.4"/></svg></button>
<button class="planbtn on"><svg class="ic" viewBox="0 0 24 24"><path d="M5 12l5 5 9-10"/></svg>В плане</button>
<button class="timebtn est">~1ч 30м</button>
</div>
<div class="f"><label>Бейджи и теги</label></div>
<div class="row wrap mt-6">
<span class="chip">chip</span>
<span class="chip gold">gold</span>
<span class="chip dng">dng</span>
<span class="chip fill">fill</span>
<span class="chip"><span class="dot" style="background:#7DD3FC"></span>с точкой</span>
<span class="tag" style="background:rgba(74,222,128,.16);color:#4ADE80"><span class="d" style="background:#4ADE80"></span>Frontend</span>
<span class="tag" style="background:rgba(255,90,78,.16);color:#FF5A4E"><span class="d" style="background:#FF5A4E"></span>Срочно</span>
</div>
<div class="f"><label>Переключатели</label></div>
<div class="row wrap mt-6 gap-14">
<div class="seg"><button class="on">Мои</button><button>Все</button><button>Архив</button></div>
<button class="switch on" role="switch" aria-checked="true"></button>
<span class="cbx on"><svg class="ic" viewBox="0 0 24 24"><path d="M5 12l5 5 9-10"/></svg></span>
<span class="cbx"><svg class="ic" viewBox="0 0 24 24"><path d="M5 12l5 5 9-10"/></svg></span>
</div>
<div class="f2">
<div class="f"><label>Поле</label><div class="in"><input placeholder="Название задачи" value="Проверить встраивание"></div></div>
<div class="f"><label>Список</label><div class="in"><select><option>Tvorymo shotam</option><option>Другой проект</option></select></div></div>
</div>
<div class="qa mt-14">
<input placeholder="Быстро добавить задачу…">
<button class="go ok"><svg class="ic" viewBox="0 0 24 24"><path d="M12 5v14M5 12h14"/></svg></button>
</div>
<div class="sec"><h2>Канбан</h2></div>
<div class="board">
<div class="col">
<div class="ch"><h2>В работе</h2><span class="n">2</span><span class="sum">3ч</span></div>
<div class="cards">
<div class="tcard plan">
<div class="t">Передавати авторизацію CRM сайтам в iframe через postMessage</div>
<div class="m"><span class="chip gold">Tvorymo shotam</span><span>сегодня</span></div>
<div class="sb"><div class="pbar"><i style="width:66%"></i></div><span class="cnt">2/3</span></div>
<div class="ft">
<span class="by"><i class="av" style="background:#FFCA40">О</i></span>
<span class="to"><i class="av" style="background:#7DD3FC">М</i><i class="av" style="background:#4ADE80">Л</i></span>
</div>
</div>
<div class="tcard over-due">
<div class="t">Подключить стили CRM на тестовой странице</div>
<div class="m"><span class="ov">просрочено</span><span class="chip dng">блокирует</span></div>
</div>
</div>
<button class="add">+ Задача</button>
</div>
<div class="col">
<div class="ch"><h2>Готово</h2><span class="n">1</span></div>
<div class="cards">
<div class="tcard done">
<div class="t">Задеплоить test-crm.tes.gd</div>
<div class="sb"><div class="pbar"><i style="width:100%"></i></div><span class="cnt full">3/3</span></div>
</div>
</div>
</div>
</div>
</div>
</main>
<script>
(() => {
// Сессия только в памяти, токен не выводится и не логируется.
let session = null; // { token, expiresAt }
let crmSource = null, crmOrigin = null, count = 0;
let crmSource = null, crmOrigin = null, count = 0, logged = false;
const $ = (id) => document.getElementById(id);
const inIframe = window.parent !== window;
function setStatus(text, cls) { const s = $("status"); s.textContent = text; s.className = cls || ""; }
function log(text) {
const li = document.createElement("li");
li.textContent = new Date().toLocaleTimeString() + " — " + text;
$("log").prepend(li);
// kind: "wait" | "ok" | "err"
function setStatus(text, kind) {
const s = $("status");
s.textContent = text;
s.className = "chip " + (kind === "ok" ? "fill" : kind === "err" ? "dng" : "gold");
$("dot").className = "syncdot" + (kind === "ok" ? "" : " off");
}
function fmtTime(v) {
if (v == null) return "—";
function log(text) {
if (!logged) { $("log").textContent = ""; logged = true; }
const row = document.createElement("div");
row.className = "arcitem";
const s = document.createElement("span"); s.className = "s";
s.textContent = new Date().toLocaleTimeString().slice(0, 5);
const t = document.createElement("span"); t.className = "t"; t.textContent = text;
row.append(s, t);
$("log").prepend(row);
}
function toDate(v) {
if (v == null) return null;
const n = Number(v);
const d = new Date(Number.isFinite(n) ? (n < 1e12 ? n * 1000 : n) : v);
return isNaN(d) ? String(v) : d.toLocaleString() + " (" + d.toISOString() + ")";
return isNaN(d) ? null : d;
}
function fmtTime(v) {
const d = toDate(v);
return d ? d.toLocaleString() + " (" + d.toISOString() + ")" : (v == null ? "—" : String(v));
}
function initials(s) {
return (s || "?").split(/[\s@._-]+/).filter(Boolean).slice(0, 2).map((w) => w[0]).join("").toUpperCase() || "?";
}
async function onAuth(data, ev) {
@@ -58,7 +198,7 @@
const prevExp = session && session.expiresAt;
session = { token, expiresAt: data.expires_at };
count++;
setStatus("проверяю токен в Supabase…");
setStatus("проверяю токен в Supabase…", "wait");
try {
const r = await fetch("/api/verify", {
method: "POST", headers: { "Content-Type": "application/json" },
@@ -68,14 +208,20 @@
if (!res.ok) { setStatus("Токен не принят: " + res.error, "err"); log("crm:auth от " + ev.origin + " — отклонён"); return; }
$("name").textContent = res.name || "имя не задано в Supabase";
$("email").textContent = res.email || "—";
$("initials").textContent = initials(res.name || res.email);
$("uid").textContent = res.id;
$("expires").textContent = fmtTime(data.expires_at != null ? data.expires_at : res.exp);
const exp = data.expires_at != null ? data.expires_at : res.exp;
$("expires").textContent = fmtTime(exp);
const d = toDate(exp);
$("mins").textContent = d ? String(Math.max(0, Math.round((d - Date.now()) / 60000))) : "—";
$("count").textContent = String(count);
$("info").hidden = false; $("refresh").hidden = false;
$("changed").textContent = changed ? "токен сменился" : count > 1 ? "тот же токен" : "токен первый";
$("changed").className = changed ? "chip fill" : "chip";
$("info").hidden = false; $("noinfo").hidden = true; $("refresh").hidden = false;
setStatus(changed ? "Токен сменился, пользователь подтверждён Supabase" : "Пользователь подтверждён Supabase", "ok");
log("crm:auth от " + ev.origin + (changed
? " — новый токен, expires_at: " + fmtTime(prevExp) + " → " + fmtTime(data.expires_at)
: session && count > 1 ? " — тот же токен" : " — принят"));
: count > 1 ? " — тот же токен" : " — принят"));
} catch (e) {
setStatus("Ошибка проверки токена", "err");
}
@@ -91,10 +237,11 @@
$("refresh").addEventListener("click", () => {
const target = crmSource || window.parent;
target.postMessage({ type: "crm:refresh" }, crmOrigin || "*");
setStatus("запросил новый токен (crm:refresh)…");
setStatus("запросил новый токен (crm:refresh)…", "wait");
log("отправлен crm:refresh → " + (crmOrigin || "*"));
});
$("mode").textContent = inIframe ? "в iframe" : "без iframe";
if (inIframe) {
window.parent.postMessage({ type: "crm:ready", version: 1 }, "*");
log("отправлен crm:ready");