import { useEffect, useMemo, useRef } from "react"; import type { FormEvent } from "react"; import { exactHtmlDocuments } from "./exact-html"; import type { ExactHtmlKey } from "./exact-html"; import type { AuthMode, NavigateFn, Page } from "./route-config"; const fileToPage: Record = { "account.html": "account", "asset-factory.html": "assetFactory", "image-optimize.html": "imageOptimize", "index.html": "dashboard", "library.html": "library", "login.html": "login", "messages.html": "messages", "model-photo.html": "modelPhoto", "model-photo-demo-a.html": "modelPhotoDemoA", "model-photo-demo-b.html": "modelPhotoDemoB", "pipeline.html": "pipeline", "platform-cover.html": "platformCover", "product-create.html": "productCreateUpload", "product-create-upload.html": "productCreateUpload", "product-detail.html": "productDetail", "products.html": "products", "projects-new.html": "projectWizard", "projects.html": "projects", "register.html": "register", "settings.html": "settings", "team.html": "team" }; const pageToExactKey: Record = { dashboard: "dashboard", products: "products", productDetail: "productDetail", productCreateUpload: "productCreateUpload", projects: "projects", projectWizard: "projectWizard", pipeline: "pipeline", library: "library", account: "account", team: "team", messages: "messages", assetFactory: "assetFactory", imageOptimize: "imageOptimize", modelPhoto: "modelPhoto", modelPhotoDemoA: "modelPhotoDemoA", modelPhotoDemoB: "modelPhotoDemoB", platformCover: "platformCover", settings: "settings", settingsNotify: "settings" }; const exactKeyToPage: Partial> = { account: "account", assetFactory: "assetFactory", dashboard: "dashboard", imageOptimize: "imageOptimize", library: "library", login: "login", messages: "messages", modelPhoto: "modelPhoto", modelPhotoDemoA: "modelPhotoDemoA", modelPhotoDemoB: "modelPhotoDemoB", pipeline: "pipeline", platformCover: "platformCover", productCreate: "productCreateUpload", productCreateUpload: "productCreateUpload", productDetail: "productDetail", products: "products", projectWizard: "projectWizard", projects: "projects", register: "register", settings: "settings", team: "team" }; const exactKeyToFile: Record = { account: "account.html", assetFactory: "asset-factory.html", dashboard: "index.html", imageOptimize: "image-optimize.html", library: "library.html", login: "login.html", messages: "messages.html", modelPhoto: "model-photo.html", modelPhotoDemoA: "model-photo-demo-a.html", modelPhotoDemoB: "model-photo-demo-b.html", pipeline: "pipeline.html", platformCover: "platform-cover.html", productCreate: "product-create.html", productCreateUpload: "product-create-upload.html", productDetail: "product-detail.html", products: "products.html", projectWizard: "projects-new.html", projects: "projects.html", register: "register.html", settings: "settings.html", team: "team.html" }; const liveHydratePages = new Set([ "dashboard", "products", "productDetail", "projectWizard", "projects", "pipeline", "library", "account", "settings", "team" ]); function routeFromHref(rawHref: string | null) { if (!rawHref || rawHref === "#" || rawHref.startsWith("javascript:")) return null; const url = new URL(rawHref, "https://airshelf.local/exact/"); const fileName = url.pathname.split("/").filter(Boolean).pop() || "index.html"; const page = fileToPage[fileName]; if (!page) return null; const params = new URLSearchParams(url.search); return { page, hash: url.hash || "", search: url.search || "", productId: params.get("product_id") || undefined, projectId: params.get("project_id") || undefined }; } function routeFromInlineAction(action: string | null) { if (!action) return null; const hrefMatch = action.match(/location\.href\s*=\s*['"]([^'"]+)/); if (hrefMatch) return routeFromHref(hrefMatch[1]); const hashMatch = action.match(/location\.hash\s*=\s*['"]([^'"]+)/); if (hashMatch) return { page: null, hash: hashMatch[1], search: "" }; return null; } function setFrameHeight(frame: HTMLIFrameElement) { frame.style.height = `${Math.max(window.innerHeight, 720)}px`; } export type ExactDocumentPageProps = { pageKey: ExactHtmlKey; hash?: string; productId?: string; projectId?: string; navigate?: NavigateFn; onAuthModeChange?: (mode: AuthMode) => void; onAuthSubmit?: (mode: AuthMode, event: FormEvent, form: HTMLFormElement) => void; }; export function exactKeyForPage(page: Page): ExactHtmlKey { return pageToExactKey[page] || "dashboard"; } function contextSearch(pageKey: ExactHtmlKey, productId?: string, projectId?: string) { const params = new URLSearchParams(); if (pageKey === "productDetail" && productId) params.set("product_id", productId); if (pageKey === "pipeline" && projectId) params.set("project_id", projectId); const text = params.toString(); return text ? `?${text}` : ""; } export function ExactDocumentPage({ pageKey, hash, productId, projectId, navigate, onAuthModeChange, onAuthSubmit }: ExactDocumentPageProps) { const frameRef = useRef(null); const html = useMemo(() => { const context = { page: exactKeyToFile[pageKey], search: contextSearch(pageKey, productId, projectId), hash: hash ? `#${hash.replace(/^#/, "")}` : "", liveHydrate: liveHydratePages.has(pageKey) }; return exactHtmlDocuments[pageKey].replace( "", `` ); }, [hash, pageKey, productId, projectId]); useEffect(() => { const frame = frameRef.current; if (!frame) return; const currentFrame: HTMLIFrameElement = frame; function onLoad() { const doc = currentFrame.contentDocument; const win = currentFrame.contentWindow; if (!doc || !win) return; (win as Window & { __AIR_SHELF_HOST_NAVIGATE__?: (href: string) => void }).__AIR_SHELF_HOST_NAVIGATE__ = ( href: string ) => { const hostRoute = routeFromHref(href); if (!hostRoute?.page) return; if (hostRoute.page === "login" || hostRoute.page === "register") { onAuthModeChange?.(hostRoute.page); return; } navigate?.(hostRoute.page, { hash: hostRoute.hash || undefined, productId: hostRoute.productId || (hostRoute.page === "productDetail" ? productId : undefined), projectId: hostRoute.projectId || (hostRoute.page === "pipeline" ? projectId : undefined) }); }; const applyFrameHash = (nextHash: string) => { const cleanHash = nextHash.replace(/^#/, ""); const stageMatch = cleanHash.match(/^stage-(\d+)$/); const pipelineWindow = win as Window & { activateStage?: (stage: number) => void }; if (pageKey === "pipeline" && stageMatch && typeof pipelineWindow.activateStage === "function") { pipelineWindow.activateStage(Number(stageMatch[1])); return; } const settingsWindow = win as Window & { showSection?: (sectionId: string) => void }; if (pageKey === "settings" && cleanHash.startsWith("sec-") && typeof settingsWindow.showSection === "function") { settingsWindow.showSection(cleanHash); return; } doc.getElementById(cleanHash)?.scrollIntoView({ behavior: "smooth", block: "start" }); }; if (hash) { setTimeout(() => { applyFrameHash(hash); }, 0); } const clickHandler = (event: MouseEvent) => { const target = event.target as Element | null; if (!target) return; const syncHashOnlyRoute = (nextHash: string) => { const cleanHash = nextHash.replace(/^#/, ""); applyFrameHash(cleanHash); if (exactKeyToPage[pageKey]) { const nextUrl = `${window.location.pathname}${window.location.search}#${cleanHash}`; window.history.replaceState(null, "", nextUrl); } }; const actionNode = target.closest("[onclick]") as HTMLElement | null; const actionRoute = routeFromInlineAction(actionNode?.getAttribute("onclick") || null); if (actionRoute?.hash && actionRoute.page === null) { event.preventDefault(); event.stopPropagation(); event.stopImmediatePropagation(); syncHashOnlyRoute(actionRoute.hash); return; } if (actionRoute?.page) { event.preventDefault(); event.stopPropagation(); event.stopImmediatePropagation(); if (actionRoute.page === "login" || actionRoute.page === "register") { onAuthModeChange?.(actionRoute.page); return; } navigate?.(actionRoute.page, { hash: actionRoute.hash || undefined, productId: actionRoute.productId || (actionRoute.page === "productDetail" ? productId || "exact" : undefined), projectId: actionRoute.projectId }); return; } const anchor = target.closest("a[href]") as HTMLAnchorElement | null; const rawAnchorHref = anchor?.getAttribute("href") || null; if (rawAnchorHref?.startsWith("#")) { event.preventDefault(); event.stopPropagation(); event.stopImmediatePropagation(); syncHashOnlyRoute(rawAnchorHref); return; } const anchorRoute = routeFromHref(rawAnchorHref); if (!anchorRoute) return; event.preventDefault(); event.stopPropagation(); if (anchorRoute.page === "login" || anchorRoute.page === "register") { onAuthModeChange?.(anchorRoute.page); return; } navigate?.(anchorRoute.page, { hash: anchorRoute.hash || undefined, productId: anchorRoute.productId || (anchorRoute.page === "productDetail" ? productId || "exact" : undefined), projectId: anchorRoute.projectId }); }; const submitHandler = (event: SubmitEvent) => { const form = event.target as HTMLFormElement | null; if (!form) return; const isLogin = pageKey === "login" && form.id === "login-form"; const isRegister = pageKey === "register" && form.id === "register-form"; if (!isLogin && !isRegister) return; event.preventDefault(); event.stopPropagation(); event.stopImmediatePropagation(); onAuthSubmit?.(isLogin ? "login" : "register", event as unknown as FormEvent, form); }; doc.addEventListener("click", clickHandler, true); doc.addEventListener("submit", submitHandler, true); setFrameHeight(currentFrame); const resizeFrame = () => setFrameHeight(currentFrame); window.addEventListener("resize", resizeFrame); const observer = new ResizeObserver(() => setFrameHeight(currentFrame)); observer.observe(doc.documentElement); observer.observe(doc.body); const cleanup = () => { doc.removeEventListener("click", clickHandler, true); doc.removeEventListener("submit", submitHandler, true); window.removeEventListener("resize", resizeFrame); observer.disconnect(); }; currentFrame.dataset.cleanupKey = String(Date.now()); (currentFrame as HTMLIFrameElement & { __airshelfCleanup?: () => void }).__airshelfCleanup?.(); (currentFrame as HTMLIFrameElement & { __airshelfCleanup?: () => void }).__airshelfCleanup = cleanup; } currentFrame.addEventListener("load", onLoad); if (currentFrame.contentDocument?.readyState !== "loading") onLoad(); return () => { currentFrame.removeEventListener("load", onLoad); (currentFrame as HTMLIFrameElement & { __airshelfCleanup?: () => void }).__airshelfCleanup?.(); (currentFrame as HTMLIFrameElement & { __airshelfCleanup?: () => void }).__airshelfCleanup = undefined; }; }, [hash, navigate, onAuthModeChange, onAuthSubmit, pageKey]); return (