diff --git a/cmd/widgetwasm/main.go b/cmd/widgetwasm/main.go index 931c4d1..222aa6f 100644 --- a/cmd/widgetwasm/main.go +++ b/cmd/widgetwasm/main.go @@ -3,6 +3,7 @@ package main import ( + "net/url" "regexp" "strings" "syscall/js" @@ -23,12 +24,128 @@ func main() { js.Global().Set("__eli_nps_wasm_decidir", js.FuncOf(decidir)) js.Global().Set("__eli_nps_wasm_cooldown_ativo", js.FuncOf(cooldownAtivo)) js.Global().Set("__eli_nps_wasm_set_cooldown", js.FuncOf(setCooldown)) + js.Global().Set("__eli_nps_wasm_painel_init", js.FuncOf(painelInit)) js.Global().Set("__eli_nps_wasm_ready", true) // Mantém o módulo vivo. select {} } +func painelInit(this js.Value, args []js.Value) any { + // Painel: persistência de filtros no localStorage. + // Regras (.agent): lógica no WASM (Go). Aqui é best-effort e fail-open. + // + // - Salva: produto selecionado + checkbox "baixas". + // - Restaura: se a URL não tiver parâmetros e houver valor salvo, + // redireciona para /painel?produto=...&baixas=1. + // + // Importante: não roda fora do /painel. + loc := js.Global().Get("location") + if loc.IsUndefined() || loc.IsNull() { + return nil + } + path := loc.Get("pathname").String() + if path == "" || !strings.HasPrefix(path, "/painel") { + return nil + } + + storage := js.Global().Get("localStorage") + if storage.IsUndefined() || storage.IsNull() { + return nil + } + + const keyProd = "eli_nps_painel_produto" + const keyBaixas = "eli_nps_painel_baixas" + + // Aguarda o DOM estar pronto para conseguirmos acessar o form/option list. + doc := js.Global().Get("document") + if doc.IsUndefined() || doc.IsNull() { + return nil + } + + handler := js.FuncOf(func(this js.Value, args []js.Value) any { + // Best-effort: evita exceptions se APIs não existirem. + defer func() { _ = recover() }() + + form := doc.Call("querySelector", `form[action="/painel"]`) + if form.IsUndefined() || form.IsNull() { + return nil + } + + sel := form.Call("querySelector", `select[name="produto"]`) + chk := form.Call("querySelector", `input[name="baixas"]`) + + // Helper: verifica se option existe. + optionExists := func(selectEl js.Value, value string) bool { + if selectEl.IsUndefined() || selectEl.IsNull() { + return false + } + opts := selectEl.Get("options") + ln := opts.Get("length").Int() + for i := 0; i < ln; i++ { + opt := opts.Index(i) + if opt.Get("value").String() == value { + return true + } + } + return false + } + + persist := func() { + if !sel.IsUndefined() && !sel.IsNull() { + v := strings.TrimSpace(sel.Get("value").String()) + if v != "" { + storage.Call("setItem", keyProd, v) + } + } + baixas := "0" + if !chk.IsUndefined() && !chk.IsNull() && chk.Get("checked").Truthy() { + baixas = "1" + } + storage.Call("setItem", keyBaixas, baixas) + } + + // Restaura/redirect apenas se não há query string. + s := loc.Get("search").String() + if strings.TrimSpace(s) == "" { + storedProd := strings.TrimSpace(storage.Call("getItem", keyProd).String()) + storedBaixas := strings.TrimSpace(storage.Call("getItem", keyBaixas).String()) + if storedProd != "" && optionExists(sel, storedProd) { + q := "?produto=" + url.QueryEscape(storedProd) + if storedBaixas == "1" { + q += "&baixas=1" + } + loc.Call("replace", path+q) + return nil + } + // Se só baixas estiver setado, aplica também. + if storedBaixas == "1" { + loc.Call("replace", path+"?baixas=1") + return nil + } + } + + // Liga listeners para persistência. + onSubmit := js.FuncOf(func(this js.Value, args []js.Value) any { persist(); return nil }) + onChange := js.FuncOf(func(this js.Value, args []js.Value) any { persist(); return nil }) + form.Call("addEventListener", "submit", onSubmit) + form.Call("addEventListener", "change", onChange) + + return nil + }) + + // DOMContentLoaded + readyState := doc.Get("readyState").String() + if readyState != "loading" { + // Se o WASM foi carregado depois do DOM pronto (comum no painel), + // rodamos imediatamente. + handler.Invoke() + } else { + doc.Call("addEventListener", "DOMContentLoaded", handler) + } + return nil +} + func cooldownAtivo(this js.Value, args []js.Value) any { if len(args) < 1 { return false diff --git a/internal/elinps/painel.go b/internal/elinps/painel.go index 5f997d4..690617b 100644 --- a/internal/elinps/painel.go +++ b/internal/elinps/painel.go @@ -321,6 +321,10 @@ th,td{padding:8px;border-bottom:1px solid #eee;text-align:left;vertical-align:to b.WriteString("") + // JS do painel: apenas bootstrap para executar a lógica no WASM. + // Regras (.agent): sem dependências externas. A lógica fica no WASM. + b.WriteString(``) + b.WriteString("") w.Write([]byte(b.String())) } diff --git a/web/static/e-li.nps.wasm b/web/static/e-li.nps.wasm index d533202..b544181 100755 Binary files a/web/static/e-li.nps.wasm and b/web/static/e-li.nps.wasm differ diff --git a/web/static/painel.js b/web/static/painel.js new file mode 100644 index 0000000..4726e5a --- /dev/null +++ b/web/static/painel.js @@ -0,0 +1,49 @@ +(function(){ + // Bootstrap mínimo do painel. + // Regra (.agent): a lógica fica no WASM; aqui apenas garantimos que o WASM + // esteja carregado/executando. + // + // Este arquivo existe para evitar JS inline no HTML do painel. + + async function carregarWasmPainel(){ + try{ + if(window.__eli_nps_wasm_ready) return true; + if(window.__eli_nps_wasm_loading) return window.__eli_nps_wasm_loading; + + window.__eli_nps_wasm_loading = (async function(){ + try{ + if(!window.Go){ + // wasm_exec.js deve ter sido carregado pelo HTML. + return false; + } + const go = new Go(); + const res = await fetch('/static/e-li.nps.wasm', {cache: 'no-cache'}); + if(!res.ok) return false; + const bytes = await res.arrayBuffer(); + const {instance} = await WebAssembly.instantiate(bytes, go.importObject); + go.run(instance); + return !!window.__eli_nps_wasm_ready; + }catch(e){ + return false; + } + })(); + + return window.__eli_nps_wasm_loading; + }catch(e){ + return false; + } + } + + document.addEventListener('DOMContentLoaded', function(){ + carregarWasmPainel().then(function(ok){ + if(!ok) return; + // Executa o init do painel no WASM (best-effort). + try{ + if(typeof window.__eli_nps_wasm_painel_init === 'function'){ + window.__eli_nps_wasm_painel_init(); + } + }catch(e){} + }); + }); +})(); +