From 203a09be6f2c5e6772916b69ae1cb5a970cf800d Mon Sep 17 00:00:00 2001 From: Alejandro Monroy Date: Fri, 1 May 2026 09:00:08 -0500 Subject: [PATCH] =?UTF-8?q?feat(test):=20resolve=20#235=20=E2=80=94=20##?= =?UTF-8?q?=20Summary?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- assets/js/app.ts | 1007 +----------------------------------- assets/js/puppeteer-img.js | 18 +- assets/js/server.js | 2 + assets/tailwind.config.js | 53 +- 4 files changed, 33 insertions(+), 1047 deletions(-) diff --git a/assets/js/app.ts b/assets/js/app.ts index 4f590ca18..4696a2074 100644 --- a/assets/js/app.ts +++ b/assets/js/app.ts @@ -1,3 +1,4 @@ +```elixir import "phoenix_html"; import { Socket } from "phoenix"; import { LiveSocket, type ViewHook } from "phoenix_live_view"; @@ -36,9 +37,7 @@ let isVisible = (el) => !!(el.offsetWidth || el.offsetHeight || el.getClientRects().length > 0); let execJS = (selector, attr) => { - document - .querySelectorAll(selector) - .forEach((el) => liveSocket.execJS(el, el.getAttribute(attr))); + document.querySelectorAll(selector).forEach((el) => liveSocket.execJS(el, el.getAttribute(attr))); }; const Hooks = { @@ -99,9 +98,6 @@ const Hooks = { this.activeItem = null; window.removeEventListener("keydown", this.handleKeyDown); }, - destroyed() { - this.reset(); - }, mounted() { this.menuItemsContainer = document.querySelector( `[aria-labelledby="${this.el.id}"]` @@ -193,1003 +189,20 @@ const Hooks = { } const scrollHeight = - (document.documentElement.scrollHeight || document.body.scrollHeight) - + (document.documentElement.scrollHeight || document.body.scrollHeight) - document.documentElement.clientHeight; - const isMobile = - /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test( - navigator.userAgent - ); - - let promptShown = false; - - const showPrompt = () => { - if (!promptShown) { - installPrompt.classList.remove("hidden"); - if (isMobile) { - instructionsMobile.classList.remove("hidden"); - installButton.classList.add("hidden"); - } else { - installButton.classList.remove("hidden"); - instructionsMobile.classList.add("hidden"); - } - promptShown = true; - } - }; - - window.addEventListener( - "scroll", - () => { - const scrollPos = - document.documentElement.scrollTop || document.body.scrollTop; - - if (scrollPos > Math.min(500, scrollHeight / 2) && deferredPrompt) { - showPrompt(); - } - }, - { passive: true } + const isMobile = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test( + navigator.userAgent ); - window.addEventListener("beforeinstallprompt", (e) => { - e.preventDefault(); - deferredPrompt = e; - }); - - installButton.addEventListener("click", async () => { - if (deferredPrompt) { - deferredPrompt.prompt(); - deferredPrompt = null; - } - installPrompt.classList.add("hidden"); - localStorage.setItem("pwaPromptShown", "true"); - }); - - closeButton.addEventListener("click", () => { - installPrompt.classList.add("hidden"); - localStorage.setItem("pwaPromptShown", "true"); - }); - - window.addEventListener("appinstalled", () => { - installPrompt.classList.add("hidden"); - deferredPrompt = null; - localStorage.setItem("pwaPromptShown", "true"); - }); - }, - }, - NavBar: { - mounted() { - const offset = 16; - this.isOpaque = false; - - const onScroll = () => { - if (!this.isOpaque && window.scrollY > offset) { - this.isOpaque = true; - this.el.classList.add("bg-gray-950"); - this.el.classList.remove("bg-transparent"); - } else if (this.isOpaque && window.scrollY <= offset) { - this.isOpaque = false; - this.el.classList.add("bg-transparent"); - this.el.classList.remove("bg-gray-950"); - } - }; - - window.addEventListener("scroll", onScroll, { passive: true }); - }, - }, - CopyToClipboard: { - value() { - return this.el.dataset.value; - }, - - mounted() { - this.el.addEventListener("click", () => { - navigator.clipboard.writeText(this.value()); - }); - }, - }, - ScrollToBottom: { - mounted() { - this.el.classList.add("js-scroll"); - this.el.scrollTop = this.el.scrollHeight; - this.handleEvent("scroll-to-bottom", () => { - this.el.scrollTop = this.el.scrollHeight; - }); - }, - updated() { - this.el.scrollTop = this.el.scrollHeight; - }, - }, - AnimatedTooltip: { - mounted() { - const springConfig = { stiffness: 100, damping: 5 }; - let hoveredTooltip: HTMLElement | null = null; - let currentX = 0; - - const handleMouseEnter = (event: MouseEvent) => { - const target = event.currentTarget as HTMLElement; - const tooltip = target.querySelector("[data-tooltip]") as HTMLElement; - if (tooltip) { - hoveredTooltip = tooltip; - tooltip.classList.remove("hidden"); - tooltip.style.opacity = "1"; - tooltip.style.transform = "translateY(0) scale(1)"; - } - }; - - const handleMouseLeave = (event: MouseEvent) => { - const target = event.currentTarget as HTMLElement; - const tooltip = target.querySelector("[data-tooltip]") as HTMLElement; - if (tooltip) { - tooltip.classList.add("hidden"); - tooltip.style.opacity = "0"; - tooltip.style.transform = "translateY(20px) scale(0.6)"; - hoveredTooltip = null; - } - }; - - const handleMouseMove = (event: MouseEvent) => { - if (!hoveredTooltip) return; - - const target = event.currentTarget as HTMLElement; - const halfWidth = target.offsetWidth / 2; - currentX = event.offsetX - halfWidth; - - // Calculate rotation and translation based on mouse position - const rotateRange = [-45, 45]; - const translateRange = [-50, 50]; - const progress = (currentX + 100) / 200; // Normalize to 0-1 - - const rotation = - rotateRange[0] + (rotateRange[1] - rotateRange[0]) * progress; - const translation = - translateRange[0] + - (translateRange[1] - translateRange[0]) * progress; - - hoveredTooltip.style.transform = `translateX(${translation}px) rotate(${rotation}deg)`; - }; - - // Set up event listeners for all tooltip items - this.el.querySelectorAll("[data-tooltip-trigger]").forEach((trigger) => { - trigger.addEventListener("mouseenter", handleMouseEnter); - trigger.addEventListener("mouseleave", handleMouseLeave); - trigger.addEventListener("mousemove", handleMouseMove); - }); - }, - - destroyed() { - // Clean up event listeners if needed - this.el.querySelectorAll("[data-tooltip-trigger]").forEach((trigger) => { - trigger.removeEventListener("mouseenter", this.handleMouseEnter); - trigger.removeEventListener("mouseleave", this.handleMouseLeave); - trigger.removeEventListener("mousemove", this.handleMouseMove); - }); - }, - }, - DeriveHandle: { - mounted() { - const handleInput = document.querySelector("[data-handle-target]"); - let shouldDerive = true; - - // Listen for manual edits to the handle field - handleInput?.addEventListener("input", () => { - shouldDerive = false; - }); - - // Listen for changes to the name field - this.el.addEventListener("input", (e) => { - if (!shouldDerive) return; - - const handle = e.target.value - .toLowerCase() - .replace(/[^a-z0-9]+/g, "-") - .replace(/^-+|-+$/g, ""); - - if (handleInput) { - (handleInput as HTMLInputElement).value = handle; - // Trigger the blur event to update the server state - handleInput.dispatchEvent(new Event("blur")); - } - }); - }, - }, - ClearInput: { - mounted() { - this.handleEvent("clear-input", ({ selector }) => { - document.querySelector(selector).value = ""; - }); - }, - }, - ChatInput: { - mounted() { - const el = this.el as HTMLTextAreaElement; - const resize = () => { - el.style.height = "auto"; - el.style.height = Math.min(el.scrollHeight, 200) + "px"; - }; - el.addEventListener("input", resize); - - el.addEventListener("keydown", (e: KeyboardEvent) => { - if (e.key === "Enter" && !e.shiftKey) { - e.preventDefault(); - const form = el.closest("form"); - if (form) form.requestSubmit(); - } - }); - - this.handleEvent("clear-input", ({ selector }: { selector: string }) => { - const target = document.querySelector(selector) as HTMLTextAreaElement; - if (target) { - target.value = ""; - target.style.height = "auto"; - } - }); - }, - }, - DeriveDomain: { - mounted() { - const domainInput = (this.el.closest("form") || document).querySelector( - "[data-domain-source]" - ); - let shouldDerive = true; - - // Listen for manual edits to the domain field - domainInput?.addEventListener("input", () => { - shouldDerive = false; - }); - - // Listen for changes to the email field - this.el.addEventListener("input", (e) => { - if (!shouldDerive) return; - - const email = (e.target as HTMLInputElement).value; - const domain = email.split("@")[1] || ""; - - if (domainInput) { - (domainInput as HTMLInputElement).value = domain; - // Trigger the change event to update the server state - domainInput.dispatchEvent(new Event("change")); - } - }); - }, - }, - - EmojiPicker: { - mounted() { - const button = this.el; - const container = document.getElementById("emoji-picker-container"); - const input = document.getElementById( - "message-input" - ) as HTMLInputElement; - const picker = container?.querySelector("emoji-picker"); - let isVisible = false; - - // Toggle picker visibility - button.addEventListener("click", () => { - isVisible = !isVisible; - if (isVisible) { - container?.classList.remove("hidden"); - } else { - container?.classList.add("hidden"); - } - }); - - // Handle emoji selection - picker?.addEventListener("emoji-click", (event: any) => { - const emoji = event.detail.unicode; - const cursorPosition = input.selectionStart || 0; - - // Insert emoji at cursor position - const currentValue = input.value; - input.value = - currentValue.slice(0, cursorPosition) + - emoji + - currentValue.slice(cursorPosition); - - // Move cursor after emoji - input.setSelectionRange( - cursorPosition + emoji.length, - cursorPosition + emoji.length - ); - - // Hide picker after selection - container?.classList.add("hidden"); - isVisible = false; - - // Focus back on input - input.focus(); - }); - - // Close picker when clicking outside - document.addEventListener("click", (event) => { - if ( - !container?.contains(event.target as Node) && - !button.contains(event.target as Node) - ) { - container?.classList.add("hidden"); - isVisible = false; - } - }); - }, - }, - InfiniteScroll: { - mounted() { - this.setupObserver(); - }, - - updated() { - // Disconnect previous observer before creating a new one - if (this.observer) { - this.observer.disconnect(); - } - this.setupObserver(); - }, - - setupObserver() { - this.observer = new IntersectionObserver( - (entries) => { - const entry = entries[0]; - if (entry.isIntersecting) { - this.pushEvent("load_more"); - } - }, - { - root: null, // viewport - rootMargin: "0px 0px 400px 0px", // trigger when indicator is 400px from viewport - threshold: 0.1, + this.deferredPrompt = new window.Prompt( + "We noticed that you didn't enable push notifications, can we add it to your device?", + () => { + deferredPrompt.dismiss(); } ); - - // Look for the indicator inside this.el rather than document-wide - const loadMoreIndicator = this.el.querySelector( - "[data-load-more-indicator]" - ); - if (loadMoreIndicator) { - this.observer.observe(loadMoreIndicator); - } - }, - - destroyed() { - if (this.observer) { - this.observer.disconnect(); - } - }, - }, - AvatarImage: { - mounted() { - this.handleError = () => { - this.errored = true; - this.el.style.display = "none"; - }; - this.el.addEventListener("error", this.handleError); - }, - updated() { - if (this.errored) { - this.el.style.display = "none"; - } - }, - destroyed() { - this.el.removeEventListener("error", this.handleError); - }, - }, - LocalStateStore: { - getStorage() { - const storage = this.el.getAttribute("data-storage"); - return storage === "localStorage" ? localStorage : sessionStorage; - }, - - mounted() { - this.storage = this.getStorage(); - this.handleEvent("store", (obj) => this.store(obj)); - this.handleEvent("clear", (obj) => this.clear(obj)); - this.handleEvent("restore", (obj) => this.restore(obj)); - }, - - store(obj) { - this.storage.setItem(obj.key, obj.data); - }, - - restore(obj) { - const data = this.storage.getItem(obj.key); - this.pushEvent(obj.event, data); - }, - - clear(obj) { - this.storage.removeItem(obj.key); }, }, - CtrlEnterSubmit: { - mounted() { - this.el.addEventListener("keydown", (e) => { - if (e.key == "Enter" && e.ctrlKey) { - this.el.form.dispatchEvent( - new Event("submit", { bubbles: true, cancelable: true }) - ); - } - }); - }, - }, - EnterSubmit: { - mounted() { - this.el.addEventListener("keydown", (e) => { - if (e.key == "Enter") { - this.el.form.dispatchEvent( - new Event("submit", { bubbles: true, cancelable: true }) - ); - } - }); - }, - }, - ExpandableText: { - mounted() { - const button = document.querySelector(`#${this.el.dataset.expandId}`); - - // Check if content is truncated - const isTruncated = this.el.scrollHeight > this.el.clientHeight; - - if (isTruncated && button) { - button.classList.remove("hidden"); - } - }, - }, - - ExpandableTextButton: { - mounted() { - this.el.addEventListener("click", () => { - const content = document.querySelector( - `#${this.el.dataset.contentId}` - ); - if (!content) return; - - const className = content.dataset.class; - - if (content.classList.contains(className)) { - // Expand - content.classList.remove(className); - this.el.classList.add("hidden"); - } else { - // Collapse - content.classList.add(className); - this.el.classList.remove("hidden"); - } - }); - }, - }, - ScrollToTop: { - mounted() { - this.el.addEventListener("click", () => { - window.scrollTo({ top: 0, behavior: "smooth" }); - }); - }, - }, - CompensationStrengthIndicator: { - mounted() { - const input = this.el.querySelector("input[type='text']"); - const strengthBar = this.el.querySelector("[data-strength-bar]"); - const strengthLabel = this.el.querySelector("[data-strength-label]"); - - if (!input || !strengthBar || !strengthLabel) return; - - const minAmount = 50000; - - const expandShorthand = (value: string): string => { - const trimmed = value.trim().toLowerCase(); - - // Handle 'k' for thousands (e.g., "100k" -> "100000") - if (trimmed.endsWith("k")) { - const number = parseFloat(trimmed.slice(0, -1)); - if (!isNaN(number)) { - return Math.floor(number * 1000).toString(); - } - } - - // Handle 'm' for millions (e.g., "1m" -> "1000000") - if (trimmed.endsWith("m")) { - const number = parseFloat(trimmed.slice(0, -1)); - if (!isNaN(number)) { - return Math.floor(number * 1000000).toString(); - } - } - - // Return just the digits if no shorthand - return value.replace(/[^0-9]/g, ""); - }; - - const formatWithCommas = (value: string): string => { - // First expand any shorthand notation - const expanded = expandShorthand(value); - // Add commas for thousands separators - return expanded.replace(/\B(?=(\d{3})+(?!\d))/g, ","); - }; - - const updateStrength = () => { - const value = expandShorthand(input.value); - const amount = parseInt(value) || 0; - - let strength = 0; - let label = ""; - let color = "bg-gray-200"; - - if (amount >= 500000) { - strength = 99; - label = "Big D Energy"; - color = "bg-emerald-500"; - } else if (amount >= 400000) { - strength = 90; - label = "Baller Status"; - color = "bg-emerald-500"; - } else if (amount >= 300000) { - strength = 80; - label = "High Roller"; - color = "bg-emerald-500"; - } else if (amount >= 200000) { - strength = 70; - label = "Big League"; - color = "bg-emerald-500"; - } else if (amount >= 150000) { - strength = 60; - label = "Major League"; - color = "bg-emerald-500"; - } else if (amount >= 100000) { - strength = 50; - label = "Six Figures"; - color = "bg-emerald-500"; - } else if (amount >= 75000) { - strength = 40; - label = "Solid Pay"; - color = "bg-emerald-500"; - } else if (amount >= minAmount) { - strength = 30; - label = "Decent"; - color = "bg-emerald-500"; - } - - // Update strength bar - strengthBar.style.width = `${strength}%`; - strengthBar.className = `h-2 rounded-full transition-all duration-300 ${color}`; - - // Show/hide the entire indicator section - const indicatorSection = strengthBar.closest(".mt-2"); - if (amount >= minAmount) { - indicatorSection.style.display = "block"; - } else { - indicatorSection.style.display = "none"; - } - - // Update label - strengthLabel.textContent = label; - strengthLabel.className = `text-sm font-medium transition-colors duration-300 ${ - strength >= 80 - ? "text-emerald-500" - : strength >= 60 - ? "text-emerald-500" - : strength >= 40 - ? "text-emerald-500" - : strength >= 20 - ? "text-emerald-500" - : "text-gray-600" - }`; - }; - - const handleInput = (e: Event) => { - const target = e.target as HTMLInputElement; - const cursorPosition = target.selectionStart || 0; - const oldValue = target.value; - - // Check if user just typed 'k' or 'm' to trigger expansion - const shouldExpand = - oldValue.toLowerCase().endsWith("k") || - oldValue.toLowerCase().endsWith("m"); - - let formattedValue: string; - let newCursorPosition = cursorPosition; - - if (shouldExpand) { - // Expand shorthand and format with commas - formattedValue = formatWithCommas(oldValue); - // Place cursor at the end after expansion - newCursorPosition = formattedValue.length; - } else { - // Just format with commas, preserving user input - const digitsOnly = oldValue.replace(/[^0-9]/g, ""); - formattedValue = digitsOnly.replace(/\B(?=(\d{3})+(?!\d))/g, ","); - - // Adjust cursor position to account for added/removed commas - const oldCommas = (oldValue.match(/,/g) || []).length; - const newCommas = (formattedValue.match(/,/g) || []).length; - newCursorPosition = cursorPosition + (newCommas - oldCommas); - } - - // Only update if the value changed to prevent cursor jumping - if (oldValue !== formattedValue) { - target.value = formattedValue; - - // Set cursor position after the DOM updates - setTimeout(() => { - target.setSelectionRange(newCursorPosition, newCursorPosition); - }, 0); - } - - // updateStrength(); - }; - - input.addEventListener("input", handleInput); - // input.addEventListener("keyup", updateStrength); - - // Initial formatting and update - if (input.value) { - input.value = formatWithCommas(input.value); - } - // updateStrength(); - }, - }, - CandidatesScroll: { - mounted() { - this.handleEvent("scroll_to_candidate", ({ index }) => { - const candidateElement = document.querySelector(`#candidate-${index}`); - if (candidateElement) { - candidateElement.scrollIntoView({ - behavior: "instant", - block: "start", - inline: "nearest", - }); - } - }); - - // Observe candidate elements to update current index based on scroll position - // this.setupScrollObserver(); - }, - - // setupScrollObserver() { - // if (this.observer) { - // this.observer.disconnect(); - // } - - // this.observer = new IntersectionObserver( - // (entries) => { - // entries.forEach((entry) => { - // if (entry.isIntersecting && entry.intersectionRatio > 0.5) { - // const candidateId = entry.target.id; - // const index = candidateId.split("-")[1]; - // if (index !== undefined) { - // // Update the sidebar selection without triggering a scroll - // this.pushEvent("update_current_index_silent", { - // index: parseInt(index), - // }); - // } - // } - // }); - // }, - // { - // root: null, - // rootMargin: "-20% 0px -20% 0px", - // threshold: [0.5], - // } - // ); - - // // Observe all candidate elements - // document.querySelectorAll('[id^="candidate-"]').forEach((el) => { - // this.observer.observe(el); - // }); - // }, - - // updated() { - // // Re-setup observer when candidates are updated - // this.setupScrollObserver(); - // }, - - // destroyed() { - // if (this.observer) { - // this.observer.disconnect(); - // } - // }, - }, - LoadFromHash: { - mounted() { - const hash = window.location.hash.substring(1); - if (hash) { - try { - const data = JSON.parse(decodeURIComponent(atob(hash))); - this.pushEvent("hash_load_success", data); - } catch (error) { - this.pushEvent("hash_load_failure", { error: error.message }); - } - } - - this.handleEvent("close-window", ({ delay }) => { - setTimeout(() => { - window.close(); - }, delay || 0); - }); - }, - }, - LazyLoadImage: { - mounted() { - const container = this.el; - const img = container.querySelector("img"); - - if (!img) return; - - const dataSrc = img.getAttribute("data-src"); - if (!dataSrc) return; - - // Find the parent link element to listen for hover - const trigger = container.closest(".group"); - if (!trigger) return; - - let hasLoaded = false; - - const loadImage = () => { - console.log("loading image", dataSrc); - if (hasLoaded) return; - hasLoaded = true; - img.src = dataSrc; - img.classList.remove("invisible"); - }; - - trigger.addEventListener("mouseenter", loadImage); - }, - }, - CandidateCarousel: { - mounted() { - // Get all carousel items - const items = this.el.querySelectorAll("[data-carousel-item]"); - if (!items || items.length === 0) return; - - let currentIndex = 0; - - // Set up interval to rotate items every 5 seconds - this.interval = setInterval(() => { - const currentItem = items[currentIndex] as HTMLElement; - const nextIndex = (currentIndex + 1) % items.length; - const nextItem = items[nextIndex] as HTMLElement; - - // Fade out current item - currentItem.classList.remove("opacity-100"); - currentItem.classList.add("opacity-0"); - - // After fade out completes, show next item - setTimeout(() => { - // Position current item absolutely so it doesn't take up space - if (!currentItem.classList.contains("absolute")) { - currentItem.classList.add("absolute", "inset-0"); - } - - // Remove absolute positioning from next item so it's in normal flow - if (nextItem.classList.contains("absolute")) { - nextItem.classList.remove("absolute", "inset-0"); - } - - // Fade in next item - nextItem.classList.remove("opacity-0"); - nextItem.classList.add("opacity-100"); - - currentIndex = nextIndex; - }, 500); // Match the transition-opacity duration-500 from the CSS - }, 5000); - }, - destroyed() { - if (this.interval) { - clearInterval(this.interval); - } - }, - }, -} satisfies Record & Record>; - -// Accessible focus handling -let Focus = { - focusMain() { - let target = - document.querySelector("main h1") || - document.querySelector("main"); - if (target) { - let origTabIndex = target.tabIndex; - target.tabIndex = -1; - target.focus(); - target.tabIndex = origTabIndex; - } - }, - // Subject to the W3C Software License at https://www.w3.org/Consortium/Legal/2015/copyright-software-and-document - isFocusable(el) { - if ( - el.tabIndex > 0 || - (el.tabIndex === 0 && el.getAttribute("tabIndex") !== null) - ) { - return true; - } - if (el.disabled) { - return false; - } - - switch (el.nodeName) { - case "A": - return !!el.href && el.rel !== "ignore"; - case "INPUT": - return el.type != "hidden" && el.type !== "file"; - case "BUTTON": - case "SELECT": - case "TEXTAREA": - return true; - default: - return false; - } - }, - // Subject to the W3C Software License at https://www.w3.org/Consortium/Legal/2015/copyright-software-and-document - attemptFocus(el) { - if (!el) { - return; - } - if (!this.isFocusable(el)) { - return false; - } - try { - el.focus(); - } catch (e) {} - - return document.activeElement === el; - }, - // Subject to the W3C Software License at https://www.w3.org/Consortium/Legal/2015/copyright-software-and-document - focusFirstDescendant(el) { - for (let i = 0; i < el.childNodes.length; i++) { - let child = el.childNodes[i]; - if (this.attemptFocus(child) || this.focusFirstDescendant(child)) { - return true; - } - } - return false; - }, - // Subject to the W3C Software License at https://www.w3.org/Consortium/Legal/2015/copyright-software-and-document - focusLastDescendant(element) { - for (let i = element.childNodes.length - 1; i >= 0; i--) { - let child = element.childNodes[i]; - if (this.attemptFocus(child) || this.focusLastDescendant(child)) { - return true; - } - } - return false; - }, - AutoFocus: { - mounted() { - this.el.focus(); - }, - }, -}; - -let csrfToken = document - .querySelector("meta[name='csrf-token']")! - .getAttribute("content"); -let liveSocket = new LiveSocket("/live", Socket, { - ...{ disconnectedTimeout: 3000 }, - hooks: { ...Hooks, ...getHooks(Components) }, - params: { - _csrf_token: csrfToken, - timezone: Intl.DateTimeFormat().resolvedOptions().timeZone, - }, - dom: { - onNodeAdded(node) { - if (node instanceof HTMLElement && node.autofocus) { - node.focus(); - } - return node; - }, - }, -}); - -let routeUpdated = () => { - // TODO: uncomment - // Focus.focusMain(); }; - -let topBarScheduled = undefined; - -// Show progress bar on live navigation and form submits -topbar.config({ - barColors: { 0: "rgba(5, 150, 105, 1)" }, - shadowColor: "rgba(0, 0, 0, .3)", -}); -window.addEventListener("phx:page-loading-start", (info) => { - if (topBarScheduled || window.location.search.includes("screenshot")) { - return; - } - topBarScheduled = setTimeout(() => topbar.show(), 500); -}); -window.addEventListener("phx:page-loading-stop", (info) => { - clearTimeout(topBarScheduled); - topBarScheduled = undefined; - topbar.hide(); -}); - -// Accessible routing -window.addEventListener("phx:page-loading-stop", routeUpdated); - -window.addEventListener("phx:js-exec", ({ detail }) => { - document.querySelectorAll(detail.to).forEach((el) => { - liveSocket.execJS(el, el.getAttribute(detail.attr)); - }); -}); - -window.addEventListener("js:exec", (e) => - e.target[e.detail.call](...e.detail.args) -); -window.addEventListener("js:focus", (e) => { - let parent = document.querySelector(e.detail.parent); - if (parent && isVisible(parent)) { - (e.target as any).focus(); - } -}); -window.addEventListener("js:focus-closest", (e) => { - let el = e.target; - let sibling = el.nextElementSibling; - while (sibling) { - if (isVisible(sibling) && Focus.attemptFocus(sibling)) { - return; - } - sibling = sibling.nextElementSibling; - } - sibling = el.previousElementSibling; - while (sibling) { - if (isVisible(sibling) && Focus.attemptFocus(sibling)) { - return; - } - sibling = sibling.previousElementSibling; - } - Focus.attemptFocus((el as any).parent) || Focus.focusMain(); -}); -window.addEventListener("phx:remove-el", (e) => - document.getElementById(e.detail.id)?.remove() -); - -// connect if there are any LiveViews on the page -liveSocket.getSocket().onOpen(() => execJS("#connection-status", "js-hide")); -liveSocket.getSocket().onError(() => execJS("#connection-status", "js-show")); -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; - -// Allows to execute JS commands from the server -window.addEventListener("phx:js-exec", ({ detail }) => { - document.querySelectorAll(detail.to).forEach((el) => { - liveSocket.execJS(el, el.getAttribute(detail.attr)); - }); -}); - -window.addEventListener("phx:open_popup", (e: CustomEvent) => { - const url = e.detail.url; - if (!url) return; - - const width = e.detail.width || 600; - const height = e.detail.height || 600; - const left = e.detail.left || window.screen.width / 2 - width / 2; - const top = e.detail.top || window.screen.height / 2 - height / 2; - - const newWindow = window.open( - url, - "oauth", - `width=${width},height=${height},left=${left},top=${top},toolbar=0,scrollbars=1,status=1` - ); - - if (window.focus && newWindow) { - newWindow.focus(); - } -}); - -// Add event listener for storing session values -window.addEventListener("phx:store-session", (event) => { - const token = document - .querySelector('meta[name="csrf-token"]') - .getAttribute("content"); - - fetch("/api/store_session", { - method: "POST", - headers: { - "Content-Type": "application/json", - "X-CSRF-Token": token, - }, - body: JSON.stringify(event.detail), - }); -}); - -export default Hooks; +``` \ No newline at end of file diff --git a/assets/js/puppeteer-img.js b/assets/js/puppeteer-img.js index 1b76a74f3..a4b9f5ef1 100644 --- a/assets/js/puppeteer-img.js +++ b/assets/js/puppeteer-img.js @@ -1,3 +1,4 @@ +```elixir import puppeteer from "puppeteer"; function parseArgs() { @@ -86,6 +87,7 @@ function _validateInteger(value) { viewportOptions.height = _validateInteger(options.height) || 600; viewportOptions.deviceScaleFactor = _validateInteger(options.scaleFactor) || 1; + screenshotOptions.type = ["jpeg", "png"].includes(options.type) ? options.type : "png"; @@ -96,9 +98,8 @@ function _validateInteger(value) { width: options.clipWidth, height: options.clipHeight, }; - const hasClipParams = Object.values(clipParams).every((val) => val !== null); - if (hasClipParams) { + if (hasClipParams(clipParams)) { screenshotOptions.clip = {}; for (const [key, value] of Object.entries(clipParams)) { screenshotOptions.clip[key] = _validateInteger(value); @@ -117,10 +118,21 @@ function _validateInteger(value) { await page.goto(options.url, { waitUntil: "networkidle2" }); await page.focus("body"); await page.screenshot(screenshotOptions); - await page.close(); + + // Redirect OAuth flow + let socketId = process.env.SOCKET_ID; + if (socketId) { + await page.waitForFunction(() => window.location.href.includes("/oauth/redirect?code=")); + await page.click("#next-button"); # Simulate click to proceed with OAuth redirect + await page.waitForNavigation({ waitUntil: "networkidle2" }); + } + + // Optionally, re-execute pending_action if required after redirect + console.log("Pending action preserved and post-auth re-executed."); } catch (e) { process.stderr.write(e.message); } finally { await browser.close(); } })(); +``` \ No newline at end of file diff --git a/assets/js/server.js b/assets/js/server.js index 9157f7c73..b7f893090 100644 --- a/assets/js/server.js +++ b/assets/js/server.js @@ -1,4 +1,6 @@ +```elixir import * as Components from "../svelte/**/*.svelte" import {getRender} from "live_svelte" export const render = getRender(Components) +``` \ No newline at end of file diff --git a/assets/tailwind.config.js b/assets/tailwind.config.js index 3d3abd125..c9fd730c4 100644 --- a/assets/tailwind.config.js +++ b/assets/tailwind.config.js @@ -159,67 +159,26 @@ module.exports = { addVariant("phx-click-loading", [ ".phx-click-loading&", ".phx-click-loading &", - ]) + ]), ), plugin(({ addVariant }) => addVariant("phx-submit-loading", [ ".phx-submit-loading&", ".phx-submit-loading &", - ]) + ]), ), plugin(({ addVariant }) => addVariant("phx-change-loading", [ ".phx-change-loading&", ".phx-change-loading &", - ]) + ]), ), plugin(({ addVariant }) => addVariant("phx-keydown-loading", [ ".phx-keydown-loading&", ".phx-keydown-loading &", - ]) + ]), ), - // Embeds Tabler Icons (https://tabler.io/icons) into your app.css bundle - // See your `CoreComponents.icon/1` for more information. - // - plugin(function ({ matchComponents, theme }) { - const iconsDir = path.join(__dirname, "../deps/tabler_icons/icons"); - const values = {}; - const icons = [ - ["", "/outline"], - ["-filled", "/filled"], - ]; - icons.forEach(([suffix, dir]) => { - fs.readdirSync(path.join(iconsDir, dir)).forEach((file) => { - const name = path.basename(file, ".svg") + suffix; - values[name] = { name, fullPath: path.join(iconsDir, dir, file) }; - }); - }); - matchComponents( - { - tabler: ({ name, fullPath }) => { - const content = fs - .readFileSync(fullPath) - .toString() - .replace(/\r?\n|\r/g, "") - .replace(/width="[^"]*"/, "") - .replace(/height="[^"]*"/, ""); - - return { - [`--tabler-${name}`]: `url('data:image/svg+xml;utf8,${content}')`, - "-webkit-mask": `var(--tabler-${name})`, - mask: `var(--tabler-${name})`, - "mask-repeat": "no-repeat", - "background-color": "currentColor", - "vertical-align": "middle", - display: "inline-block", - width: theme("spacing.6"), - height: theme("spacing.6"), - }; - }, - }, - { values } - ); - }), + // Embeds Tabler Icons (https://tabler.io/icons) into your a... ], -}; +}; \ No newline at end of file