diff --git a/frontend/index.html b/frontend/index.html index 91c509ec..f2ca0abd 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -23,18 +23,34 @@ - - + }; + + if (document.readyState === "complete") { + scheduleAdsense(); + } else { + window.addEventListener("load", scheduleAdsense, { once: true }); + } + +
diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index 428d0383..8fb9d774 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -1,19 +1,7 @@ -import { useState, useEffect, useCallback, useRef, useMemo } from "react"; -import GA4React from "ga-4-react"; -import Editor from "@monaco-editor/react"; -import formatXML from "xml-formatter"; -import { initializeApp } from "firebase/app"; -import { - getAuth, - GoogleAuthProvider, - signInWithPopup, - signOut, -} from "firebase/auth"; +import { useState, useEffect, useCallback, useRef, useMemo, lazy, Suspense } from "react"; import logo from "./logo.svg"; import TabsNav from "./components/TabsNav"; import DataPipelineHeader from "./components/DataPipelineHeader"; -import FeedbackWidget from "./components/FeedbackWidget"; -import BuyMeACoffee from "./components/BuyMeACoffee"; import Icon from "./components/Icon"; import { parseErrorLines, @@ -26,6 +14,42 @@ import { /* global __APP_VERSION__ */ +const MonacoEditor = lazy(() => import("@monaco-editor/react")); +const FeedbackWidget = lazy(() => import("./components/FeedbackWidget")); +const BuyMeACoffee = lazy(() => import("./components/BuyMeACoffee")); + +function runWhenIdle(callback, timeout = 2000) { + if (typeof window === "undefined") { + return () => {}; + } + if ("requestIdleCallback" in window) { + const id = window.requestIdleCallback(callback, { timeout }); + return () => window.cancelIdleCallback?.(id); + } + const id = window.setTimeout(callback, timeout); + return () => window.clearTimeout(id); +} + +function Editor({ height, ...props }) { + const fallbackStyle = height ? { height } : undefined; + return ( + + Loading editor... + + } + > + + + ); +} + function debounce(fn, delay) { let t; return (...args) => { @@ -273,6 +297,7 @@ export default function App() { const [viewportWidth, setViewportWidth] = useState(() => typeof window !== "undefined" ? window.innerWidth : 0, ); + const [widgetsReady, setWidgetsReady] = useState(false); const ethicalSlotRef = useRef(null); const isLocalhost = typeof window !== "undefined" && @@ -291,12 +316,32 @@ export default function App() { const ethicalAdVariant = `${isCompactEthicalAd ? "compact" : "wide"}-${ethicalAdType}`; const backendBase = (env.VITE_BACKEND_URL || "").replace(/\/$/, ""); - console.log("Using this URL as backendURL:", backendBase); useEffect(() => { - const ga4react = new GA4React(env.VITE_GA_ID); - ga4react.initialize().catch(err => console.error(err)); - }, []); + const gaId = env.VITE_GA_ID; + if (!gaId) return; + let cancelled = false; + const cancelIdle = runWhenIdle(async () => { + if (cancelled) return; + try { + const { default: GA4React } = await import("ga-4-react"); + if (cancelled) return; + const ga4react = new GA4React(gaId); + ga4react.initialize().catch((err) => console.error(err)); + } catch (err) { + console.error(err); + } + }, 2500); + return () => { + cancelled = true; + cancelIdle?.(); + }; + }, []); + + useEffect(() => { + const cancelIdle = runWhenIdle(() => setWidgetsReady(true), 2000); + return () => cancelIdle?.(); + }, []); // Persist workspace on change useEffect(() => { @@ -597,16 +642,28 @@ export default function App() { useEffect(() => { if (!goPro) return; - try { - const cfg = env.VITE_FIREBASE_CONFIG; - if (cfg) { + let cancelled = false; + const cancelIdle = runWhenIdle(async () => { + if (cancelled) return; + try { + const cfg = env.VITE_FIREBASE_CONFIG; + if (!cfg) return; + const [{ initializeApp }, { getAuth }] = await Promise.all([ + import("firebase/app"), + import("firebase/auth"), + ]); + if (cancelled) return; const app = initializeApp(JSON.parse(cfg)); const a = getAuth(app); setAuth(a); a.onAuthStateChanged((u) => setUser(u)); - } - } catch {} - }, []); + } catch {} + }, 2500); + return () => { + cancelled = true; + cancelIdle?.(); + }; + }, [goPro]); useEffect(() => { tabsRef.current = tabs; @@ -1174,11 +1231,14 @@ export default function App() { ); useEffect(() => { - if (adsenseClient && adsenseSlot && window.adsbygoogle) { + if (!adsenseClient || !adsenseSlot) return; + const cancelIdle = runWhenIdle(() => { + if (!window.adsbygoogle) return; try { window.adsbygoogle.push({}); } catch {} - } + }, 2000); + return () => cancelIdle?.(); }, []); useEffect(() => { @@ -1190,25 +1250,36 @@ export default function App() { setEthicalAdsReady(true); return; } - const existing = document.querySelector("script[data-ethicalads]"); - const script = existing || document.createElement("script"); - if (!existing) { - script.src = "https://media.ethicalads.io/media/client/ethicalads.min.js"; - script.async = true; - script.dataset.ethicalads = "true"; - document.body.appendChild(script); - } - const onLoad = () => setEthicalAdsReady(Boolean(window.ethicalads)); - const onError = () => setEthicalAdsReady(false); - script.addEventListener("load", onLoad); - script.addEventListener("error", onError); - const fallback = window.setTimeout(() => { - if (!window.ethicalads) setEthicalAdsReady(false); - }, 3500); + let cancelled = false; + let script = null; + let fallback = null; + let onLoad = null; + let onError = null; + const loadEthicalAds = () => { + if (cancelled) return; + const existing = document.querySelector("script[data-ethicalads]"); + script = existing || document.createElement("script"); + if (!existing) { + script.src = "https://media.ethicalads.io/media/client/ethicalads.min.js"; + script.async = true; + script.dataset.ethicalads = "true"; + document.body.appendChild(script); + } + onLoad = () => setEthicalAdsReady(Boolean(window.ethicalads)); + onError = () => setEthicalAdsReady(false); + script.addEventListener("load", onLoad); + script.addEventListener("error", onError); + fallback = window.setTimeout(() => { + if (!window.ethicalads) setEthicalAdsReady(false); + }, 3500); + }; + const cancelIdle = runWhenIdle(loadEthicalAds, 2000); return () => { - script.removeEventListener("load", onLoad); - script.removeEventListener("error", onError); - window.clearTimeout(fallback); + cancelled = true; + cancelIdle?.(); + if (script && onLoad) script.removeEventListener("load", onLoad); + if (script && onError) script.removeEventListener("error", onError); + if (fallback) window.clearTimeout(fallback); }; }, [ethicalAdsEnabled]); @@ -1220,17 +1291,6 @@ export default function App() { } catch {} }, [ethicalAdsEnabled, ethicalAdsReady, ethicalAdVariant]); - useEffect(() => { - if (!ethicalAdsPublisher) return; - const existing = document.querySelector("script[data-ethicalads]"); - if (existing) return; - const script = document.createElement("script"); - script.src = "https://media.ethicalads.io/media/client/ethicalads.min.js"; - script.async = true; - script.dataset.ethicalads = "true"; - document.body.appendChild(script); - }, []); - return (
{ethicalAdsEnabled && ( @@ -1284,6 +1344,7 @@ export default function App() { accept="application/json,.json" className="file-input" onChange={handleWorkspaceImport} + aria-label="Import workspace file" />
@@ -1336,6 +1397,7 @@ export default function App() { placeholder="Parameter name" value={p.name} onChange={(e) => updateParam(i, "name", e.target.value)} + aria-label="Parameter name" /> @@ -1543,6 +1619,7 @@ export default function App() { } }} type="button" + aria-label={showRawTrace ? "Hide raw trace output" : "Show raw trace output"} > @@ -1639,7 +1716,7 @@ export default function App() { }} > {error && !errorCollapsed && ( -
+
Errors
@@ -1710,7 +1787,9 @@ export default function App() { {showResultPane && ( <> {duration !== null && ( -
Success in {duration} ms
+
+ Success in {duration} ms +
)}
{canRenderHtml && ( @@ -1742,9 +1821,10 @@ export default function App() {
) : ( @@ -1800,7 +1881,7 @@ export default function App() {
- logo + XSLT Playground logo xsltplayground.com
-
{traceHover.text}
)} - - + {widgetsReady && ( + + + + + )}
); } diff --git a/frontend/src/components/BuyMeACoffee.jsx b/frontend/src/components/BuyMeACoffee.jsx index c7fc1228..3fcf999f 100644 --- a/frontend/src/components/BuyMeACoffee.jsx +++ b/frontend/src/components/BuyMeACoffee.jsx @@ -9,32 +9,51 @@ export default function BuyMeACoffee() { if (!containerRef.current) return; if (document.getElementById(SCRIPT_ID)) return; - const script = document.createElement("script"); - script.id = SCRIPT_ID; - script.setAttribute("data-name", "BMC-Widget"); - script.src = "https://cdnjs.buymeacoffee.com/1.0.0/widget.prod.min.js"; - script.setAttribute("data-id", "alexandrev"); - script.setAttribute("data-description", "Support me on Buy me a coffee!"); - script.setAttribute( - "data-message", - "Thanks for using XSLT Playground. If it helped you, feel free to buy me a coffee ☕", - ); - script.setAttribute("data-color", "#FFDD00"); - script.setAttribute("data-position", "Right"); - script.setAttribute("data-x_margin", "24"); - script.setAttribute("data-y_margin", "24"); - script.async = true; - containerRef.current.appendChild(script); + let cancelled = false; + let script = null; + let cancelIdle = null; + let handleLoad = null; + const loadWidget = () => { + if (cancelled || !containerRef.current) return; + script = document.createElement("script"); + script.id = SCRIPT_ID; + script.setAttribute("data-name", "BMC-Widget"); + script.src = "https://cdnjs.buymeacoffee.com/1.0.0/widget.prod.min.js"; + script.setAttribute("data-id", "alexandrev"); + script.setAttribute("data-description", "Support me on Buy me a coffee!"); + script.setAttribute( + "data-message", + "Thanks for using XSLT Playground. If it helped you, feel free to buy me a coffee ☕", + ); + script.setAttribute("data-color", "#FFDD00"); + script.setAttribute("data-position", "Right"); + script.setAttribute("data-x_margin", "24"); + script.setAttribute("data-y_margin", "24"); + script.async = true; + containerRef.current.appendChild(script); - const handleLoad = () => { - const evt = document.createEvent("Event"); - evt.initEvent("DOMContentLoaded", false, false); - window.dispatchEvent(evt); + handleLoad = () => { + const evt = document.createEvent("Event"); + evt.initEvent("DOMContentLoaded", false, false); + window.dispatchEvent(evt); + }; + script.addEventListener("load", handleLoad); }; - script.addEventListener("load", handleLoad); + if ("requestIdleCallback" in window) { + const idleId = window.requestIdleCallback(loadWidget, { timeout: 2500 }); + cancelIdle = () => window.cancelIdleCallback?.(idleId); + } else { + const timerId = window.setTimeout(loadWidget, 1800); + cancelIdle = () => window.clearTimeout(timerId); + } + return () => { - script.removeEventListener("load", handleLoad); + cancelled = true; + cancelIdle?.(); + if (script && handleLoad) { + script.removeEventListener("load", handleLoad); + } }; }, []); diff --git a/frontend/src/components/FeedbackWidget.jsx b/frontend/src/components/FeedbackWidget.jsx index 449bf832..c13c10ad 100644 --- a/frontend/src/components/FeedbackWidget.jsx +++ b/frontend/src/components/FeedbackWidget.jsx @@ -225,8 +225,6 @@ export default function FeedbackWidget() {
Feedback