{
const t = new Uint32Array(256);
for(let i=0;i<256;i++){
let c = i;
for(let k=0;k<8;k++) c = (c & 1) ? (0xEDB88320 ^ (c >>> 1)) : (c >>> 1);
t[i] = c >>> 0;
}
return t;
})();
function crc32(buf){
let c = 0xFFFFFFFF;
for(let i=0;i>> 8);
return (c ^ 0xFFFFFFFF) >>> 0;
}
function makeZip(files){
const enc = new TextEncoder();
const parts = [], central = [];
let offset = 0;
files.forEach(f => {
const name = enc.encode(f.name), data = f.data, crc = crc32(data);
const lh = new DataView(new ArrayBuffer(30));
lh.setUint32(0, 0x04034b50, true);
lh.setUint16(4, 20, true);
lh.setUint32(14, crc, true);
lh.setUint32(18, data.length, true);
lh.setUint32(22, data.length, true);
lh.setUint16(26, name.length, true);
parts.push(new Uint8Array(lh.buffer), name, data);
const cd = new DataView(new ArrayBuffer(46));
cd.setUint32(0, 0x02014b50, true);
cd.setUint16(4, 20, true);
cd.setUint16(6, 20, true);
cd.setUint32(16, crc, true);
cd.setUint32(20, data.length, true);
cd.setUint32(24, data.length, true);
cd.setUint16(28, name.length, true);
cd.setUint32(42, offset, true);
central.push(new Uint8Array(cd.buffer), name);
offset += 30 + name.length + data.length;
});
const cdSize = central.reduce((a,b)=>a+b.length, 0);
const end = new DataView(new ArrayBuffer(22));
end.setUint32(0, 0x06054b50, true);
end.setUint16(8, files.length, true);
end.setUint16(10, files.length, true);
end.setUint32(12, cdSize, true);
end.setUint32(16, offset, true);
return new Blob([...parts, ...central, new Uint8Array(end.buffer)], {type:"application/zip"});
}
const SITE_FILES = ["manifest.json","sw.js","icon-180.png","icon-192.png","icon-512.png","icon-maskable.png"];
async function publishSite(){
if(!location.protocol.startsWith("http")){
toast("Buka dulu versi online-nya di Netlify, baru terbitkan dari sana");
return;
}
toast("Menyiapkan berkas…");
const base = location.href.replace(/[^/]*$/, "");
let src;
try{
const res = await fetch(base + "index.html", {cache:"no-store"});
if(!res.ok) throw new Error("gagal");
src = await res.text();
}catch(err){
toast("Tidak bisa membaca berkas aplikasi. Muat ulang halaman lalu coba lagi.");
return;
}
if(!src.includes("/*__CFG_START__*/") || !src.includes("/*__PRD_START__*/")){
toast("Berkas aplikasi versi lama — unggah dulu ZIP versi terbaru");
return;
}
const stamp = Date.now();
src = src.replace(/\/\*__CFG_START__\*\/[\s\S]*?\/\*__CFG_END__\*\//,
"/*__CFG_START__*/\nconst DEFAULT_CONFIG = " + safeJSON({...CONFIG, buildAt:stamp}) + ";\n/*__CFG_END__*/");
src = src.replace(/\/\*__PRD_START__\*\/[\s\S]*?\/\*__PRD_END__\*\//,
"/*__PRD_START__*/\nconst DEFAULT_PRODUCTS = " + safeJSON(PRODUCTS) + ";\n/*__PRD_END__*/");
const files = [{ name:"index.html", data:new TextEncoder().encode(src) }];
const hilang = [];
for(const f of SITE_FILES){
try{
const r = await fetch(base + f, {cache:"no-store"});
if(!r.ok) throw new Error("404");
files.push({ name:f, data:new Uint8Array(await r.arrayBuffer()) });
}catch(err){ hilang.push(f); }
}
if(hilang.length){
toast("Berkas hilang dari situs: " + hilang.join(", ") + " — unggah ZIP lengkap dulu");
return;
}
const a = document.createElement("a");
a.href = URL.createObjectURL(makeZip(files));
a.download = "hypeofbreakssilence.zip";
document.body.appendChild(a); a.click(); a.remove();
setTimeout(()=>URL.revokeObjectURL(a.href), 6000);
CONFIG.buildAt = stamp;
await save();
toast("ZIP terunduh — seret ke Netlify, jangan dibuka dulu");
}
/* ============================================================
CADANGAN
Dua lapis pengaman supaya pengaturan & katalog tidak pernah
benar-benar hilang:
1. Otomatis — begitu aplikasi versi baru menimpa yang lama,
pengaturan lama disalin dulu ke "hbs_prev" dan bisa
dipulihkan lewat satu tombol.
2. Manual — berkas .json yang bisa kamu simpan sendiri dan
dipulihkan di perangkat mana pun.
============================================================ */
async function backupDownload(){
const data = {
app: "hypeofbreakssilence",
versi: 2,
dibuat: new Date().toISOString(),
config: CONFIG,
products: PRODUCTS,
orders: ORDERS,
users: USERS
};
const d = new Date();
const nama = "hbs-cadangan-" + d.getFullYear()
+ String(d.getMonth()+1).padStart(2,"0")
+ String(d.getDate()).padStart(2,"0") + ".json";
const a = document.createElement("a");
a.href = URL.createObjectURL(new Blob([JSON.stringify(data)], {type:"application/json"}));
a.download = nama;
document.body.appendChild(a); a.click(); a.remove();
setTimeout(()=>URL.revokeObjectURL(a.href), 5000);
toast("Cadangan tersimpan: " + nama);
}
async function backupRestore(file){
let d;
try{ d = JSON.parse(await file.text()); }
catch(err){ toast("Berkas rusak atau bukan berkas cadangan"); return; }
if(!d || d.app !== "hypeofbreakssilence"){ toast("Ini bukan berkas cadangan HBS"); return; }
const ringkas = [
(d.products||[]).length + " artikel",
(d.orders||[]).length + " pesanan",
(d.users||[]).length + " reseller"
].join(", ");
if(!(await ask("Pulihkan " + ringkas + " dari cadangan tanggal "
+ new Date(d.dibuat).toLocaleDateString("id-ID")
+ "? Data yang ada sekarang akan diganti.", "Pulihkan cadangan", "Pulihkan"))) return;
if(d.config){ CONFIG = {...DEFAULT_CONFIG, ...d.config, buildAt: DEFAULT_CONFIG.buildAt}; }
if(d.products && d.products.length) PRODUCTS = d.products;
if(d.orders) ORDERS = d.orders;
if(d.users) USERS = d.users;
await save();
render();
toast("Cadangan dipulihkan");
}
async function restorePrev(){
const prev = await store.get("hbs_prev");
if(!prev || !prev.config){ toast("Tidak ada pengaturan lama"); return; }
if(!(await ask("Pulihkan pengaturan dan katalog dari sebelum pembaruan aplikasi?",
"Pulihkan pengaturan lama", "Pulihkan"))) return;
CONFIG = {...DEFAULT_CONFIG, ...prev.config, buildAt: DEFAULT_CONFIG.buildAt};
if(prev.products && prev.products.length) PRODUCTS = prev.products;
await save();
state.hasPrev = false;
render();
toast("Pengaturan lama kembali — jangan lupa Terbitkan");
}
/* ============================================================
INPUT / CHANGE
============================================================ */
document.addEventListener("input", (e)=>{
const t = e.target;
if(t.id === "q"){
state.q = t.value;
const list = visibleProducts(), g = document.querySelector(".grid");
if(g && list.length) g.innerHTML = list.map(cardHTML).join("");
else { render(); const i = $("#q"); if(i){ i.focus(); i.setSelectionRange(i.value.length,i.value.length); } }
return;
}
if(t.dataset.in){
const p = prod(state.sheet), s = t.dataset.in, st = stockOf(p,s);
const v = Math.max(0, Math.min(st, parseInt(t.value||"0",10) || 0));
if(v) state.draft[s] = v; else delete state.draft[s];
const pcs = Object.values(state.draft).reduce((a,b)=>a+b,0);
const f = document.querySelector(".run-foot");
if(f){ f.querySelector(".t").textContent = pcs+" pcs"; f.querySelector(".v").textContent = rp(pcs*eff(p)); }
const ab = $("#addBtn");
if(ab){ ab.disabled = pcs===0; ab.textContent = pcs===0 ? "Isi jumlah dulu" : "Tambah "+pcs+" pcs ke order"; }
return;
}
if(t.id === "s-dim"){
const lab = t.parentElement && t.parentElement.querySelector("label.fl");
if(lab) lab.textContent = "Kegelapan lapisan banner — sekarang " + t.value + "%";
return;
}
if(t.id === "e-one"){
$$(".szbox").forEach(x=>x.classList.toggle("hide", t.checked));
$$(".onebox").forEach(x=>x.classList.toggle("hide", !t.checked));
}
});
document.addEventListener("change", async (e)=>{
const t = e.target;
if(t.id === "sort"){ state.sort = t.value; render(); return; }
if(t.id === "fit"){ state.fit = t.value; render(); return; }
if(t.id === "memMode"){ state.memMode = t.value; render(); return; }
if(t.id === "memYear"){ state.memYear = t.value; render(); return; }
if(t.id === "memMonth"){ state.memMonth = t.value; render(); return; }
/* provinsi dipilih → daftar kota ikut berubah */
if(t.dataset.region){
const pre = t.dataset.region, ks = $("#"+pre+"-kota"), km = $("#"+pre+"-kota-m");
if(ks) ks.innerHTML = cityOptions(t.value, "");
if(km){ km.value = ""; km.classList.add("hide"); }
if(state.view === "checkout"){ state.buyer.prov = t.value; state.buyer.kota = ""; }
return;
}
/* pilih "Lainnya" → kotak ketik manual muncul */
if(t.dataset.city){
const km = $("#"+t.dataset.city+"-kota-m");
if(km){
const manual = t.value === "__manual";
km.classList.toggle("hide", !manual);
if(manual) km.focus();
}
if(state.view === "checkout") readBuyer();
return;
}
if(t.id === "e-cat"){
const subs = subsOf(t.value), sel = $("#e-sub");
if(sel) sel.innerHTML = `` + subs.map(s=>``).join("");
return;
}
if(t.dataset.st){
const o = ORDERS.find(x=>x.no===t.dataset.st);
if(!o) return;
const lama = o.status;
o.status = t.value;
if(ONLINE){
try{ await sbSetStatus(o.no, t.value); }
catch(err){ o.status = lama; t.value = lama; toast(err.message); return; }
}
await save();
toast(o.no+" → "+statusLabel(o.status));
return;
}
if(t.id === "e-img" && t.files && t.files.length){
const room = 6 - state.editImgs.length;
if(room <= 0){ toast("Maksimal 6 foto"); t.value=""; return; }
toast(ONLINE ? "Mengunggah foto…" : "Memproses foto…");
for(const f of Array.from(t.files).slice(0, room)){
try{
const kecil = await readImg(f, 1100);
state.editImgs.push(ONLINE ? await sbUploadImage(kecil, "product-images", "produk") : kecil);
}catch(err){ toast(f.name + ": " + err.message); }
}
t.value = ""; render();
toast(state.editImgs.length + " foto siap" + (ONLINE ? " (tersimpan di server)" : ""));
return;
}
if(t.id === "bkUp" && t.files && t.files[0]){
await backupRestore(t.files[0]); t.value = ""; return;
}
if(t.id === "e-chart" && t.files && t.files[0]){
try{
const kecil = await readImg(t.files[0], 1400);
state.editChart = ONLINE ? await sbUploadImage(kecil, "size-charts", "chart") : kecil;
render(); toast("Size chart terpasang");
}catch(err){ toast(err.message); }
return;
}
if(t.dataset.pmimg !== undefined && t.files && t.files[0]){
toast(ONLINE ? "Mengunggah gambar promo…" : "Memproses gambar…");
try{
$$("[data-pm]").forEach(i => { CONFIG.promos[i.dataset.pm][i.dataset.k] = i.value; });
const kecil = await readImg(t.files[0], 1200);
CONFIG.promos[Number(t.dataset.pmimg)].img = ONLINE ? await sbUploadImage(kecil, "banner-images", "promo") : kecil;
await save(); render(); toast("Gambar promo terpasang");
}catch(err){ toast(err.message); }
return;
}
if(t.dataset.slimg !== undefined && t.files && t.files[0]){
toast("Memproses gambar banner…");
try{
$$("[data-sl]").forEach(i => { CONFIG.slides[i.dataset.sl][i.dataset.k] = i.value; });
const sl = CONFIG.slides[Number(t.dataset.slimg)];
const kecil = await readImg(t.files[0], 1920);
sl.ar = await ukurGambar(kecil);
sl.img = ONLINE ? await sbUploadImage(kecil, "banner-images", "banner") : kecil;
await save(); render(); toast("Gambar banner terpasang");
}catch(err){ toast(err.message); }
return;
}
});
async function doLogout(){
if(!state.user) return;
if(ONLINE) await sbLogout();
state.user = null; state.adminOk = false; state.cart = {};
if(ONLINE){ ORDERS = []; USERS = []; }
await saveSession(); go("land"); toast("Kamu sudah keluar");
}
document.addEventListener("keydown", e => {
if(e.key === "Escape"){ if($("#lb").classList.contains("on")) closeLightbox(); else closePanels(); }
if($("#lb").classList.contains("on")){
if(e.key === "ArrowRight") moveLightbox(1);
if(e.key === "ArrowLeft") moveLightbox(-1);
}
if(e.key === "Enter"){
if(state.view === "auth"){
const b = state.authMode === "register" ? $("#doRegister") : $("#doLogin");
if(b && document.activeElement && document.activeElement.tagName === "INPUT") b.click();
}
}
/* jalan pintas darurat kalau tombol logout mogok */
if(e.ctrlKey && e.shiftKey && (e.key === "L" || e.key === "l")){
e.preventDefault();
doLogout();
}
});
/* tekan lama tombol logout ~500ms = langsung keluar tanpa dialog */
let holdT = null;
document.addEventListener("pointerdown", e => {
const btn = e.target.closest("#logoutTop");
if(!btn) return;
holdT = setTimeout(() => { holdT = null; doLogout(); }, 500);
});
document.addEventListener("pointerup", () => { if(holdT){ clearTimeout(holdT); holdT = null; } });
document.addEventListener("pointercancel", () => { if(holdT){ clearTimeout(holdT); holdT = null; } });
document.addEventListener("pointerleave", () => { if(holdT){ clearTimeout(holdT); holdT = null; } });
/* ============================================================
BOOT
============================================================ */
(async function init(){
const sc = await store.get("hbs_config");
const sp = await store.get("hbs_products");
const so = await store.get("hbs_orders");
const su = await store.get("hbs_users");
const ss = await store.get("hbs_session");
/* Kalau file di server sudah diterbitkan ulang (buildAt berubah),
salinan lama di browser reseller dibuang supaya mereka benar-benar
melihat katalog dan pengaturan yang terbaru. Pesanan dan akun
mereka tidak ikut terhapus. */
const fresh = sc && sc.buildAt === DEFAULT_CONFIG.buildAt;
/* Aplikasi versi baru menimpa yang lama: simpan dulu pengaturan
lama supaya bisa dipulihkan satu tombol dari menu Admin. */
if(sc && !fresh){
await store.set("hbs_prev", {config:sc, products:sp || [], at:Date.now()});
state.hasPrev = true;
}else{
const prev = await store.get("hbs_prev");
state.hasPrev = !!(prev && prev.config);
}
CONFIG = fresh ? {...DEFAULT_CONFIG, ...sc} : {...DEFAULT_CONFIG};
if(!CONFIG.cats || !CONFIG.cats.length) CONFIG.cats = DEFAULT_CONFIG.cats;
if(!CONFIG.slides || !CONFIG.slides.length) CONFIG.slides = DEFAULT_CONFIG.slides;
if(!CONFIG.fits || !CONFIG.fits.length) CONFIG.fits = DEFAULT_CONFIG.fits;
if(!CONFIG.orderDisc) CONFIG.orderDisc = DEFAULT_CONFIG.orderDisc;
if(!CONFIG.ticker) CONFIG.ticker = DEFAULT_CONFIG.ticker;
if(CONFIG.bannerDim === undefined) CONFIG.bannerDim = DEFAULT_CONFIG.bannerDim;
if(CONFIG.bannerFit === undefined) CONFIG.bannerFit = DEFAULT_CONFIG.bannerFit;
if(CONFIG.phoneLock === undefined) CONFIG.phoneLock = DEFAULT_CONFIG.phoneLock;
if(!CONFIG.bannerShape) CONFIG.bannerShape = DEFAULT_CONFIG.bannerShape;
if(CONFIG.showCats === undefined) CONFIG.showCats = DEFAULT_CONFIG.showCats;
if(!CONFIG.perks) CONFIG.perks = DEFAULT_CONFIG.perks;
if(!CONFIG.promos) CONFIG.promos = [];
if(!CONFIG.landNote) CONFIG.landNote = DEFAULT_CONFIG.landNote;
PRODUCTS = (fresh && sp && sp.length) ? sp : JSON.parse(JSON.stringify(DEFAULT_PRODUCTS));
ORDERS = so || [];
USERS = su || [];
if(!fresh){ await store.set("hbs_config", CONFIG); await store.set("hbs_products", PRODUCTS); }
await initSB();
if(ONLINE){
ORDERS = []; USERS = [];
try{
const { data } = await SB.auth.getSession();
if(data && data.session){
state.user = await sbMyProfile(data.session.user.id);
fillBuyer(state.user);
state.view = "home";
}
}catch(err){ console.warn("sesi:", err.message); }
render();
if(state.user){ await sbSync(); render(); }
return;
}
// mode lokal (server belum diatur atau tidak bisa dihubungi)
if(ss){ const u = USERS.find(x=>x.id===ss); if(u){ state.user = u; fillBuyer(u); state.view = "home"; } }
render();
/* daftarkan service worker supaya aplikasi bisa dipasang di layar
depan HP dan tetap terbuka walau sinyal hilang. Hanya jalan kalau
file di-hosting (https), diabaikan diam-diam kalau dibuka lokal. */
if("serviceWorker" in navigator && location.protocol.startsWith("http")){
navigator.serviceWorker.register("sw.js").catch(()=>{});
}
})();