К
Каталог продукции
заказ в один клик
Корзина пуста
0
Оформить заказ
QR-код каталога
×
Каталог продукции
наведите камеру телефона
Разместите код на упаковке, витрине, визитке или в прайсе — клиент откроет каталог и отправит заказ прямо из телефона.
Управление каталогом
×
Товары
Загрузка списком
Фото
Настройки
//,
// поэтому публикуем только содержимое head и body — ровно так же, как
// build.ps1. Полный документ получался «документом внутри документа»,
// и у клиентов такая версия не открывалась («Couldn't load this Artifact»).
// метки собраны из кусков: в исходнике каждая должна встречаться один раз
var H = "", E = ":END-->";
var head = sliceTpl(tpl, H + S, H + E);
var body = sliceTpl(tpl, B + S, B + E);
var frag = head.trim() + "\n\n" + body.trim() + "\n";
return frag
.replace(MARK_DATA, function(){ return json; })
.replace(MARK_TPL, function(){ return escScript(tpl); });
}
/* ---- публикация на свой сервер: отправляем данные, не страницу.
Сервер сам сохранит новые фото в img/ и пересоберёт index.html. ---- */
function serverPublish(){
var token = load(LS.token, null);
if (!token){
token = window.prompt("Пароль публикации (выдан при установке сервера):", "");
if (!token){ setPublishState("Публикация отменена", "err"); return; }
token = token.trim();
}
admin.publishing = true;
setPublishState("Публикую…", "");
CONFIG.rev = String(Date.now());
var ctrl = typeof AbortController === "function" ? new AbortController() : null;
var timer = ctrl ? setTimeout(function(){ ctrl.abort(); }, 120000) : null;
fetch("api/publish", {
method:"POST",
headers:{"Content-Type":"application/json", "X-Catalog-Token":token},
body:dataJson(),
signal: ctrl ? ctrl.signal : undefined
}).then(function(r){
if (timer) clearTimeout(timer);
if (r.status === 401 || r.status === 403){ drop(LS.token); throw {code:"auth"}; }
if (r.status === 409){ throw {code:"conflict"}; }
if (!r.ok){ throw {code:"http", status:r.status}; }
return r.json();
}).then(function(res){
save(LS.token, token);
if (res && res.products && res.config){
// сервер вернул те же данные, но вместо вшитых фото — пути к файлам
CONFIG = res.config; PRODUCTS = res.products;
}
admin.publishing = false;
admin.dirty = false; drop(LS.draft);
refreshAll();
setPublishState("Опубликовано — клиенты видят новую версию", "ok");
renderAdminFoot();
}).catch(function(err){
if (timer) clearTimeout(timer);
admin.publishing = false;
var code = (err && err.code) || "";
var msg = code === "auth" ? "Неверный пароль публикации"
: code === "conflict" ? "Каталог уже обновили в другом окне — перезагрузите страницу"
: code === "http" ? ("Сервер ответил ошибкой " + err.status)
: "Сервер недоступен. Проверьте связь и попробуйте ещё раз";
setPublishState(msg, "err");
renderAdminFoot();
});
}
function publish(){
if (admin.publishing) return;
if (SERVER){ serverPublish(); return; }
if (!admin.api){ setPublishState("Публикация недоступна в этом просмотре", "err"); return; }
admin.publishing = true;
setPublishState("Публикую…", "");
var html;
try{ html = buildHtml(); }
catch(e){
admin.publishing = false;
setPublishState("Не удалось собрать страницу: " + e.message, "err");
return;
}
admin.api.publish(html).then(function(){
admin.dirty = false; drop(LS.draft);
setPublishState("Опубликовано — клиенты видят новую версию", "ok");
renderNotices();
}).catch(function(err){
admin.publishing = false;
var code = (err && err.code) || "";
var msg = code === "conflict"
? "Каталог уже обновили в другом окне — страница сейчас перезагрузится"
: (code === "not_writer" || code === "not_granted")
? "Нет прав на публикацию в этом просмотре"
: "Опубликовать не удалось. Попробуйте ещё раз";
setPublishState(msg, "err");
renderAdminFoot();
});
}
function setPublishState(text, cls){
var el = byId("publishState");
if (!el) return;
el.textContent = text;
el.className = "publish-state" + (cls ? " " + cls : "");
}
/* ---- вкладки ---- */
function openAdmin(tab){
admin.tab = tab || admin.tab;
paintTabs(); renderAdmin(); openSheet(byId("adminSheet"));
}
byId("adminOpen").addEventListener("click", function(){ openAdmin("goods"); });
Array.prototype.forEach.call(byId("adminTabs").children, function(b){
b.addEventListener("click", function(){
admin.tab = b.dataset.tab; admin.editing = null;
paintTabs(); renderAdmin();
});
});
function paintTabs(){
Array.prototype.forEach.call(byId("adminTabs").children, function(b){
b.setAttribute("aria-selected", String(b.dataset.tab === admin.tab));
});
}
function renderAdmin(){
var body = byId("adminBody");
body.textContent = "";
if (admin.tab === "goods") body.appendChild(tabGoods());
else if (admin.tab === "import") body.appendChild(tabImport());
else if (admin.tab === "photos") body.appendChild(tabPhotos());
else body.appendChild(tabSettings());
renderAdminFoot();
}
function renderAdminFoot(){
var foot = byId("adminFoot");
foot.textContent = "";
var b = document.createElement("button");
b.type = "button"; b.className = "btn-primary"; b.style.width = "100%";
b.textContent = admin.dirty ? "Опубликовать изменения" : "Всё опубликовано";
b.disabled = !admin.dirty;
b.addEventListener("click", publish);
foot.appendChild(b);
var s = document.createElement("p");
s.id = "publishState"; s.className = "publish-state";
s.textContent = admin.dirty
? "Изменения пока видите только вы"
: "Каталог совпадает с тем, что видят клиенты";
foot.appendChild(s);
}
/* ---- вкладка: товары ---- */
var aQuery = "";
function tabGoods(){
var box = document.createElement("div");
if (admin.editing !== null) return goodsForm(admin.editing);
var tools = document.createElement("div");
tools.className = "a-tools";
tools.innerHTML =
'Поиск товара ' +
'
';
var addBtn = document.createElement("button");
addBtn.type = "button"; addBtn.className = "btn-ghost"; addBtn.textContent = "+ Товар";
addBtn.addEventListener("click", function(){ admin.editing = "new"; renderAdmin(); });
tools.appendChild(addBtn);
box.appendChild(tools);
var aqi = tools.querySelector("#aq");
aqi.value = aQuery;
aqi.addEventListener("input", function(){ aQuery = aqi.value; redrawGoodsList(); });
var meter = document.createElement("div");
meter.className = "meter";
meter.id = "goodsMeter";
box.appendChild(meter);
var ul = document.createElement("ul");
ul.className = "a-list"; ul.id = "goodsList";
box.appendChild(ul);
setTimeout(redrawGoodsList, 0);
return box;
}
function redrawGoodsList(){
var ul = byId("goodsList");
if (!ul) return;
var q = aQuery.trim().toLowerCase();
var items = PRODUCTS.filter(function(p){
if (!q) return true;
return ((p.name||"") + " " + (p.sku||"") + " " + (p.cat||"")).toLowerCase().indexOf(q) !== -1;
});
ul.textContent = "";
items.slice(0, 200).forEach(function(p){
var li = document.createElement("li");
var b = document.createElement("button");
b.type = "button"; b.className = "a-item btn";
b.innerHTML =
' ' +
'' + esc(p.name) + ' ' +
'' + esc(p.sku) + ' · ' + esc(p.cat || "без категории") +
(p.sub ? ' · ' + esc(p.sub) : '') + ' ' +
'' + (hasPrice(p) ? money(p.price) : "по запросу") + ' ';
b.addEventListener("click", function(){
admin.editing = p.id; renderAdmin();
});
li.appendChild(b);
ul.appendChild(li);
});
if (!items.length){
var e = document.createElement("div");
e.className = "empty";
e.innerHTML = PRODUCTS.length ? "Не найдено Измените запрос."
: "Товаров пока нет Добавьте вручную или загрузите списком.";
ul.appendChild(e);
}
var m = byId("goodsMeter");
if (m){
var bytes = pageBytes(), pct = Math.min(100, bytes / 16777216 * 100);
m.className = "meter" + (pct > 85 ? " over" : pct > 60 ? " hot" : "");
m.innerHTML =
'Позиций: ' + PRODUCTS.length + ' ' +
(items.length !== PRODUCTS.length ? ' · показано ' + items.length : '') + ' ' +
'Вес страницы ' + mb(bytes) + ' из 16 МБ
' +
'
';
}
}
function goodsForm(which){
var isNew = which === "new";
var p = isNew
? {id:"", sku:"", name:"", cat:"", sub:"", price:0, unit:"шт", step:1, spec:"", stock:"ok", img:""}
: JSON.parse(JSON.stringify(known[which] || {}));
var box = document.createElement("div");
var back = document.createElement("button");
back.type = "button"; back.className = "btn-ghost";
back.textContent = "\u2190 К списку";
back.style.marginBottom = "12px";
back.addEventListener("click", function(){ admin.editing = null; renderAdmin(); });
box.appendChild(back);
var fs = document.createElement("fieldset");
fs.innerHTML =
'' + (isNew ? "Новый товар" : "Редактирование") + ' ' +
'' +
'' +
' ' +
'Вид внутри категории ' +
' ' +
'Второй ряд фильтров внутри марки. Можно оставить пустым
' +
' ' +
'Название
' +
'' +
'' +
'
Кратность заказа ' +
' ' +
'Шаг «плюс/минус» в карточке
' +
'
Наличие ' +
'В наличии Под заказ
' +
'
' +
'Характеристика
';
box.appendChild(fs);
var dl = fs.querySelector("#catList");
categories().forEach(function(c){
var o = document.createElement("option"); o.value = c; dl.appendChild(o);
});
var dl2 = fs.querySelector("#subList");
var allSubs = [];
PRODUCTS.forEach(function(x){ if (x.sub && allSubs.indexOf(x.sub) === -1) allSubs.push(x.sub); });
allSubs.forEach(function(s){
var o = document.createElement("option"); o.value = s; dl2.appendChild(o);
});
fs.querySelector("#gSku").value = p.sku || "";
fs.querySelector("#gCat").value = p.cat || "";
fs.querySelector("#gSub").value = p.sub || "";
fs.querySelector("#gName").value = p.name || "";
fs.querySelector("#gPrice").value = p.price || 0;
fs.querySelector("#gUnit").value = p.unit || "шт";
fs.querySelector("#gStep").value = p.step || 1;
fs.querySelector("#gStock").value = p.stock === "order" ? "order" : "ok";
fs.querySelector("#gSpec").value = p.spec || "";
var file = fs.querySelector("#gFile");
fs.querySelector("#gPick").addEventListener("click", function(){ file.click(); });
file.addEventListener("change", function(){
if (!file.files || !file.files[0]) return;
shrink(file.files[0]).then(function(url){
p.img = url;
fs.querySelector("#gImg").src = url;
fs.querySelector("#gDrop").hidden = false;
}).catch(function(e){ toast(e.message || "Не удалось обработать фото"); });
});
fs.querySelector("#gDrop").addEventListener("click", function(){
p.img = "";
fs.querySelector("#gImg").src = placeholder(p);
fs.querySelector("#gDrop").hidden = true;
});
var actions = document.createElement("div");
actions.className = "a-tools";
actions.style.marginTop = "16px";
var saveB = document.createElement("button");
saveB.type = "button"; saveB.className = "btn-primary"; saveB.style.flex = "1";
saveB.textContent = isNew ? "Добавить товар" : "Сохранить";
saveB.addEventListener("click", function(){
var sku = fs.querySelector("#gSku").value.trim();
var name = fs.querySelector("#gName").value.trim();
if (!name){ toast("Укажите название товара"); return; }
if (!sku) sku = "ART-" + (PRODUCTS.length + 1);
p.sku = sku;
p.name = name;
p.cat = fs.querySelector("#gCat").value.trim() || "Прочее";
p.sub = fs.querySelector("#gSub").value.trim();
p.price = Number(fs.querySelector("#gPrice").value) || 0;
p.unit = fs.querySelector("#gUnit").value.trim() || "шт";
p.step = Math.max(1, parseInt(fs.querySelector("#gStep").value, 10) || 1);
p.stock = fs.querySelector("#gStock").value === "order" ? "order" : "ok";
p.spec = fs.querySelector("#gSpec").value.trim();
if (isNew){
p.id = freshId(sku);
PRODUCTS.push(p);
} else {
var i = PRODUCTS.findIndex(function(x){ return x.id === which; });
if (i >= 0) PRODUCTS[i] = p;
}
admin.editing = null;
markDirty(); renderAdmin();
toast(isNew ? "Товар добавлен" : "Изменения сохранены");
});
actions.appendChild(saveB);
if (!isNew){
var del = document.createElement("button");
del.type = "button"; del.className = "btn-danger";
del.textContent = "Удалить";
del.addEventListener("click", function(){
if (del.dataset.armed !== "1"){
del.dataset.armed = "1";
del.textContent = "Точно удалить?";
setTimeout(function(){ del.dataset.armed = "0"; del.textContent = "Удалить"; }, 4000);
return;
}
PRODUCTS = PRODUCTS.filter(function(x){ return x.id !== which; });
delete cart[which]; save(LS.cart, cart);
admin.editing = null;
markDirty(); renderAdmin();
toast("Товар удалён");
});
actions.appendChild(del);
}
box.appendChild(actions);
return box;
}
function freshId(sku){
var base = String(sku).replace(/[^0-9A-Za-zА-Яа-яЁё_\-]/g, "_") || "p";
var id = base, i = 2;
while (known[id] || PRODUCTS.some(function(x){ return x.id === id; })){ id = base + "_" + (i++); }
return id;
}
/* ---- вкладка: загрузка списком ---- */
function tabImport(){
var box = document.createElement("div");
box.innerHTML =
'' +
'Скопируйте нужные строки прямо из Excel (вместе с заголовком) и вставьте сюда. ' +
'Порядок колонок: Артикул, Название, Категория, Цена, Единица, Кратность, Характеристика, Наличие, Вид . ' +
'Последняя колонка «Вид» задаёт второй ряд фильтров внутри категории и может быть пустой.
' +
'Данные из таблицы ' +
'
' +
'
';
var ta = box.querySelector("#impText");
var prev = box.querySelector("#impPreview");
var parsed = [];
ta.addEventListener("input", function(){
parsed = parseTable(ta.value);
prev.textContent = "";
if (!parsed.length){
if (ta.value.trim()){
var e = document.createElement("div");
e.className = "disabled-note";
e.textContent = "Не удалось разобрать ни одной строки. Проверьте, что колонки разделены табуляцией или точкой с запятой.";
prev.appendChild(e);
}
return;
}
var info = document.createElement("div");
info.className = "meter";
info.innerHTML = 'Распознано позиций ' + parsed.length + '
';
prev.appendChild(info);
var ul = document.createElement("ul");
ul.className = "a-list";
parsed.slice(0, 4).forEach(function(p){
var li = document.createElement("li");
li.className = "a-item";
li.innerHTML = ' ' +
'' + esc(p.name) + ' ' +
'' + esc(p.sku) + ' · ' + esc(p.cat) + ' · ' + esc(p.unit) +
' · кратн. ' + p.step + ' ' +
'' + money(p.price) + ' ';
ul.appendChild(li);
});
if (parsed.length > 4){
var more = document.createElement("p");
more.style.cssText = "font-size:12px;color:var(--muted);text-align:center;margin:8px 0 0";
more.textContent = "…и ещё " + (parsed.length - 4);
ul.appendChild(more);
}
prev.appendChild(ul);
var acts = document.createElement("div");
acts.className = "a-tools"; acts.style.marginTop = "14px";
var repl = document.createElement("button");
repl.type = "button"; repl.className = "btn-primary"; repl.style.flex = "1";
repl.textContent = "Заменить весь каталог";
repl.addEventListener("click", function(){
if (repl.dataset.armed !== "1"){
repl.dataset.armed = "1";
repl.textContent = "Заменить " + PRODUCTS.length + " поз. на " + parsed.length + "?";
setTimeout(function(){ repl.dataset.armed = "0"; repl.textContent = "Заменить весь каталог"; }, 5000);
return;
}
PRODUCTS = parsed.map(function(p){ p.id = freshId(p.sku); return p; });
cart = {}; save(LS.cart, cart);
CONFIG.demo = false;
ta.value = ""; prev.textContent = "";
markDirty(); renderAdmin();
toast("Загружено позиций: " + PRODUCTS.length);
});
var app = document.createElement("button");
app.type = "button"; app.className = "btn-ghost";
app.textContent = "Добавить к текущим";
app.addEventListener("click", function(){
parsed.forEach(function(p){ p.id = freshId(p.sku); PRODUCTS.push(p); reindex(); });
CONFIG.demo = false;
ta.value = ""; prev.textContent = "";
markDirty(); renderAdmin();
toast("Добавлено позиций: " + parsed.length);
});
acts.appendChild(repl); acts.appendChild(app);
prev.appendChild(acts);
});
return box;
}
function parseTable(text){
var rows = String(text).split(/\r?\n/).filter(function(l){ return l.trim(); });
if (!rows.length) return [];
var delim = rows[0].indexOf("\t") !== -1 ? "\t" : (rows[0].indexOf(";") !== -1 ? ";" : ",");
var out = [];
rows.forEach(function(line, i){
var c = line.split(delim).map(function(s){ return s.trim().replace(/^"|"$/g, ""); });
if (i === 0 && /артикул|название|sku|наименование/i.test(line)) return;
var name = c[1] || "";
if (!name) return;
var price = parseFloat(String(c[3] || "0").replace(/[^\d,\.\-]/g, "").replace(",", ".")) || 0;
var step = parseInt(String(c[5] || "1").replace(/[^\d]/g, ""), 10) || 1;
out.push({
id:"", sku:c[0] || "", name:name, cat:c[2] || "Прочее",
price:price, unit:c[4] || "шт", step:Math.max(1, step),
spec:c[6] || "", stock:/заказ|нет|ожид/i.test(c[7] || "") ? "order" : "ok",
sub:c[8] || "", img:""
});
});
return out;
}
/* ---- вкладка: фото ---- */
function tabPhotos(){
var box = document.createElement("div");
var without = PRODUCTS.filter(function(p){ return !p.img; });
var bytes = pageBytes(), pct = Math.min(100, bytes / 16777216 * 100);
box.innerHTML =
' 60 ? " hot" : "") + '">' +
'
С фото: ' + (PRODUCTS.length - without.length) +
' из ' + PRODUCTS.length + ' Вес страницы ' + mb(bytes) + ' из 16 МБ
' +
'
' +
'' +
'Выберите сразу много файлов — каждый привяжется к товару по имени файла, совпадающему с артикулом ' +
'(УП-1204.jpg → артикул УП-1204). Фото сжимаются автоматически.
' +
' ' +
'Загрузить фото пачкой Имя файла = артикул товара
' +
'
';
var inp = box.querySelector("#bulkFiles");
box.querySelector("#bulkDrop").addEventListener("click", function(){ inp.click(); });
inp.addEventListener("change", function(){
var files = Array.prototype.slice.call(inp.files || []);
if (!files.length) return;
var res = box.querySelector("#bulkResult");
res.innerHTML = 'Обрабатываю… ' +
'0 / ' + files.length + '
';
var done = 0, matched = 0, missed = [];
var bySku = {};
PRODUCTS.forEach(function(p){ bySku[String(p.sku).toLowerCase()] = p; });
var chain = Promise.resolve();
files.forEach(function(f){
chain = chain.then(function(){
var base = f.name.replace(/\.[^.]+$/, "").trim().toLowerCase();
var p = bySku[base];
if (!p){ missed.push(f.name); done++; tick(); return; }
return shrink(f).then(function(url){
p.img = url; matched++; done++; tick();
}).catch(function(){ missed.push(f.name); done++; tick(); });
});
});
chain.then(function(){
markDirty();
var html = 'Привязано фото ' +
'' + matched + '
';
if (missed.length){
html += 'Не нашли товар для ' + missed.length +
' файлов — проверьте, что имя файла совпадает с артикулом:' +
esc(missed.slice(0, 8).join(", ")) + (missed.length > 8 ? " …" : "") + '
';
}
res.innerHTML = html;
renderAdminFoot();
});
function tick(){
var el = byId("bulkN");
if (el) el.textContent = done + " / " + files.length;
}
});
if (without.length){
var t = document.createElement("p");
t.style.cssText = "margin:18px 0 8px;font-size:11px;text-transform:uppercase;letter-spacing:.09em;color:var(--muted);font-weight:700";
t.textContent = "Без фото — " + without.length;
box.appendChild(t);
var ul = document.createElement("ul");
ul.className = "a-list";
without.slice(0, 40).forEach(function(p){
var li = document.createElement("li");
var b = document.createElement("button");
b.type = "button"; b.className = "a-item btn";
b.innerHTML = ' ' +
'' + esc(p.name) + ' ' +
'' + esc(p.sku) + ' \u2192 ';
b.addEventListener("click", function(){
admin.tab = "goods"; admin.editing = p.id; paintTabs(); renderAdmin();
});
li.appendChild(b); ul.appendChild(li);
});
box.appendChild(ul);
}
return box;
}
function shrink(file){
return new Promise(function(res, rej){
if (!/^image\//.test(file.type)) { rej(new Error("Это не изображение")); return; }
var fr = new FileReader();
fr.onerror = function(){ rej(new Error("Не удалось открыть файл")); };
fr.onload = function(){
var im = new Image();
im.onerror = function(){ rej(new Error("Не удалось прочитать изображение")); };
im.onload = function(){
var max = 560;
var s = Math.min(1, max / Math.max(im.width, im.height));
var w = Math.max(1, Math.round(im.width * s));
var h = Math.max(1, Math.round(im.height * s));
var c = document.createElement("canvas");
c.width = w; c.height = h;
var ctx = c.getContext("2d");
ctx.fillStyle = "#ffffff"; ctx.fillRect(0, 0, w, h);
ctx.drawImage(im, 0, 0, w, h);
try{ res(c.toDataURL("image/jpeg", 0.72)); }
catch(e){ rej(new Error("Не удалось сжать фото")); }
};
im.src = fr.result;
};
fr.readAsDataURL(file);
});
}
/* ---- вкладка: настройки ---- */
function tabSettings(){
var box = document.createElement("div");
var fs = document.createElement("fieldset");
fs.innerHTML =
'Компания ' +
'Название
' +
'Подпись под названием
' +
'Телефон в подвале
';
box.appendChild(fs);
var fs2 = document.createElement("fieldset");
fs2.innerHTML =
'Куда приходят заказы ' +
'WhatsApp Только цифры со кодом страны: без плюса, пробелов и скобок
' +
'Telegram Имя пользователя без @. Оставьте пустым, если канал не нужен
';
box.appendChild(fs2);
var fs3 = document.createElement("fieldset");
fs3.innerHTML =
'Цены и заказ ' +
'' +
'Ссылка для QR-кода ' +
'Постоянный адрес каталога — менять не нужно
' +
' ' +
'Скрывать от клиентов позиции «под заказ» ' +
'
' +
' ' +
'Показывать плашку «демонстрационные товары»
';
box.appendChild(fs3);
var map = {sBrand:"brand", sTag:"tagline", sPhone:"phone", sWa:"whatsapp",
sTg:"telegram", sCur:"currency", sUrl:"catalogUrl"};
Object.keys(map).forEach(function(id){
var el = box.querySelector("#" + id);
el.value = CONFIG[map[id]] || "";
el.addEventListener("input", function(){
var v = el.value;
if (map[id] === "whatsapp") v = v.replace(/\D/g, "");
if (map[id] === "telegram") v = v.replace(/^@/, "").trim();
CONFIG[map[id]] = v;
markDirty();
});
});
var mn = box.querySelector("#sMin");
mn.value = Number(CONFIG.minOrderSum) || 0;
mn.addEventListener("input", function(){
CONFIG.minOrderSum = Number(mn.value) || 0; markDirty();
});
var hd = box.querySelector("#sHide");
var hint = box.querySelector("#hideHint");
function paintHint(){
var off = PRODUCTS.filter(function(p){ return p.stock !== "ok"; }).length;
hint.textContent = CONFIG.hideOutOfStock
? "Сейчас скрыто " + off + " из " + PRODUCTS.length + ". В админке они остаются видны вам."
: "Клиенты видят все " + PRODUCTS.length + " позиций, включая " + off + " под заказ.";
}
hd.checked = !!CONFIG.hideOutOfStock;
hd.addEventListener("change", function(){
CONFIG.hideOutOfStock = hd.checked; paintHint(); markDirty();
});
paintHint();
var dm = box.querySelector("#sDemo");
dm.checked = !!CONFIG.demo;
dm.addEventListener("change", function(){ CONFIG.demo = dm.checked; markDirty(); });
return box;
}
/* ===================== запуск ===================== */
paintBrand(); renderChips(); renderList(); renderBar(); renderNotices();
loadPacks();
function enableAdmin(){
admin.can = true;
byId("adminOpen").hidden = false;
var d = load(LS.draft, null);
if (d && d.products && d.config){
// Черновик начат на другой версии каталога: с тех пор страницу
// переопубликовали, и его применение затёрло бы новые данные.
if (String(d.rev || "") !== String(CONFIG.rev || "")){
drop(LS.draft);
admin.staleDraft = true;
} else {
CONFIG = d.config; PRODUCTS = d.products; admin.dirty = true;
}
refreshAll();
}
}
if (SERVER){
// На своём сервере админка открывается адресом с #admin; пароль
// публикации спрашивается при первой отправке и запоминается в браузере.
var wantAdmin = location.hash === "#admin" || !!load(LS.token, null);
if (wantAdmin) enableAdmin();
window.addEventListener("hashchange", function(){
if (location.hash === "#admin" && !admin.can) enableAdmin();
});
} else if (window.claude && typeof window.claude.use === "function"){
window.claude.use("user").then(function(u){
var can = false;
try{ can = !!(u && (u.canEdit() || u.isOwner())); }catch(e){ can = false; }
if (!can) return null;
enableAdmin();
return window.claude.use("artifact");
}).then(function(a){
if (a) admin.api = a;
}).catch(function(){});
}
})();