// If you want to use Phoenix channels, run `mix help phx.gen.channel` // to get started and then uncomment the line below. // import "./user_socket.js" // You can include dependencies in two ways. // // The simplest option is to put them in assets/vendor and // import them using relative paths: // // import "../vendor/some-package.js" // // Alternatively, you can `npm install some-package --prefix assets` and import // them using a path starting with the package name: // // import "some-package" // // If you have dependencies that try to import CSS, esbuild will generate a separate `app.css` file. // To load it, simply add a second `` to your `root.html.heex` file. // Include phoenix_html to handle method=PUT/DELETE in forms and buttons. import "phoenix_html" // Establish Phoenix Socket and LiveView configuration. import {Socket} from "phoenix" import {LiveSocket} from "phoenix_live_view" import {hooks as colocatedHooks} from "phoenix-colocated/who_need_help" import topbar from "../vendor/topbar" import {Hooks, mountStaticAidMaps} from "./hooks" const systemTheme = () => matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light" const setTheme = theme => { if (theme === "system") { localStorage.removeItem("phx:theme") document.documentElement.setAttribute("data-theme", systemTheme()) document.documentElement.setAttribute("data-theme-source", "system") } else { localStorage.setItem("phx:theme", theme) document.documentElement.setAttribute("data-theme", theme) document.documentElement.setAttribute("data-theme-source", "user") } } if (!document.documentElement.hasAttribute("data-theme")) { setTheme(localStorage.getItem("phx:theme") || "system") } window.addEventListener("storage", event => { if (event.key === "phx:theme") setTheme(event.newValue || "system") }) window.addEventListener("phx:set-theme", event => setTheme(event.target.dataset.phxTheme)) matchMedia("(prefers-color-scheme: dark)").addEventListener("change", () => { if (document.documentElement.getAttribute("data-theme-source") === "system") { document.documentElement.setAttribute("data-theme", systemTheme()) } }) const csrfToken = document.querySelector("meta[name='csrf-token']").getAttribute("content") const activateProtectedTokenFragment = () => { if (!window.location.hash.startsWith("#token=")) return const token = new URLSearchParams(window.location.hash.slice(1)).get("token") window.history.replaceState(null, "", `${window.location.pathname}${window.location.search}`) if (!token || !/^[A-Za-z0-9_-]{43}$/.test(token)) return const candidates = [ { form: document.getElementById("magic-link-fragment-form"), input: document.getElementById("magic-link-fragment-token"), options: document.getElementById("standard-login-options") }, { form: document.getElementById("email-change-fragment-form"), input: document.getElementById("email-change-fragment-token") } ] const candidate = candidates.find(({form, input}) => form instanceof HTMLFormElement && input instanceof HTMLInputElement ) if (!candidate) return candidate.input.value = token candidate.form.hidden = false if (candidate.options instanceof HTMLElement) candidate.options.hidden = true candidate.form.querySelector("button")?.focus() } activateProtectedTokenFragment() window.addEventListener("hashchange", activateProtectedTokenFragment) mountStaticAidMaps(document) const liveSocket = new LiveSocket("/live", Socket, { longPollFallbackMs: 2500, params: { _csrf_token: csrfToken, client_type: typeof window.WhoNeedHelpAndroid?.postMessage === "function" ? "android" : "browser" }, hooks: {...colocatedHooks, ...Hooks}, }) // Show progress bar on live navigation and form submits topbar.config({barColors: {0: "#29d"}, shadowColor: "rgba(0, 0, 0, .3)"}) window.addEventListener("phx:page-loading-start", _info => topbar.show(300)) window.addEventListener("phx:page-loading-stop", _info => topbar.hide()) window.addEventListener("phx:reset-message-form", ({detail}) => { const form = document.getElementById(detail.id) if (form instanceof HTMLFormElement) form.reset() }) window.addEventListener("phx:native-tracking-start", ({detail}) => { window.WhoNeedHelpAndroid?.postMessage?.(JSON.stringify({ action: "start", assignment_id: detail.assignment_id, csrf_token: csrfToken })) }) window.addEventListener("phx:native-tracking-stop", () => { window.WhoNeedHelpAndroid?.postMessage?.(JSON.stringify({action: "stop"})) }) const configureNativeGoogleAuth = () => { if (typeof window.WhoNeedHelpAndroid?.postMessage !== "function") return const forms = document.querySelectorAll("form[data-native-google-flow]") forms.forEach(form => { if (!(form instanceof HTMLFormElement) || form.dataset.nativeGoogleBound === "true") return form.dataset.nativeGoogleBound = "true" form.addEventListener("submit", event => { event.preventDefault() if (form.dataset.nativeGooglePending === "true") return const flow = form.dataset.nativeGoogleFlow if (!["login", "register", "link"].includes(flow)) return form.dataset.nativeGooglePending = "true" form.querySelectorAll("button").forEach(button => { button.disabled = true }) let status = form.querySelector("[data-native-google-status]") if (!(status instanceof HTMLElement)) { status = document.createElement("p") status.dataset.nativeGoogleStatus = "true" status.className = "mt-2 text-center text-sm text-base-content/70" status.setAttribute("role", "status") status.setAttribute("aria-live", "polite") form.append(status) } status.textContent = "" const localeInput = form.querySelector("input[name='google_registration[locale]']") const locale = localeInput instanceof HTMLInputElement ? localeInput.value : "" window.WhoNeedHelpAndroid.postMessage(JSON.stringify({ action: "google_sign_in", flow, locale, csrf_token: csrfToken })) }) }) } configureNativeGoogleAuth() window.addEventListener("wnh:native-google-error", () => { document.querySelectorAll("form[data-native-google-flow]").forEach(form => { if (!(form instanceof HTMLFormElement) || form.dataset.nativeGooglePending !== "true") return delete form.dataset.nativeGooglePending form.querySelectorAll("button").forEach(button => { button.disabled = false }) const status = form.querySelector("[data-native-google-status]") if (status instanceof HTMLElement) { status.textContent = form.dataset.nativeGoogleError || "Google sign-in could not be completed." status.classList.add("text-error") } }) }) document.addEventListener("submit", event => { const form = event.target if (!(form instanceof HTMLFormElement)) return const action = new URL(form.action, window.location.origin) if (action.origin === window.location.origin && action.pathname === "/users/log-out") { window.WhoNeedHelpAndroid?.postMessage?.(JSON.stringify({action: "google_sign_out"})) } }) // connect if there are any LiveViews on the page liveSocket.connect() // expose liveSocket on window for web console debug logs and latency simulation: // >> liveSocket.enableDebug() // >> liveSocket.enableLatencySim(1000) // enabled for duration of browser session // >> liveSocket.disableLatencySim() window.liveSocket = liveSocket if ("serviceWorker" in navigator) { window.addEventListener("load", () => { navigator.serviceWorker.register("/sw.js").catch(error => { console.warn("Service worker registration failed", error) }) }) } // The lines below enable quality of life phoenix_live_reload // development features: // // 1. stream server logs to the browser console // 2. click on elements to jump to their definitions in your code editor // if (process.env.NODE_ENV === "development") { window.addEventListener("phx:live_reload:attached", ({detail: reloader}) => { // Enable server log streaming to client. // Disable with reloader.disableServerLogs() reloader.enableServerLogs() // Open configured PLUG_EDITOR at file:line of the clicked element's HEEx component // // * click with "c" key pressed to open at caller location // * click with "d" key pressed to open at function component definition location let keyDown window.addEventListener("keydown", e => keyDown = e.key) window.addEventListener("keyup", _e => keyDown = null) window.addEventListener("click", e => { if(keyDown === "c"){ e.preventDefault() e.stopImmediatePropagation() reloader.openEditorAtCaller(e.target) } else if(keyDown === "d"){ e.preventDefault() e.stopImmediatePropagation() reloader.openEditorAtDef(e.target) } }, true) window.liveReloader = reloader }) }