"use client"; import { useRef, useState, useEffect } from "react"; import { useUpdateEffect } from "react-use"; import classNames from "classnames"; import { cn } from "@/lib/utils"; import { GridPattern } from "@/components/magic-ui/grid-pattern"; import { useEditor } from "@/hooks/useEditor"; import { useAi } from "@/hooks/useAi"; import { htmlTagToText } from "@/lib/html-tag-to-text"; import { AnimatedBlobs } from "@/components/animated-blobs"; import { AiLoading } from "../ask-ai/loading"; import { defaultHTML } from "@/lib/consts"; import { HistoryNotification } from "../history-notification"; import { api } from "@/lib/api"; import { toast } from "sonner"; export const Preview = ({ isNew }: { isNew: boolean }) => { const { project, device, isLoadingProject, currentTab, currentCommit, setCurrentCommit, currentPageData, pages, setPages, setCurrentPage, isSameHtml, } = useEditor(); const { isEditableModeEnabled, setSelectedElement, isAiWorking, globalAiLoading, } = useAi(); const iframeRef = useRef(null); const [hoveredElement, setHoveredElement] = useState<{ tagName: string; rect: { top: number; left: number; width: number; height: number }; } | null>(null); const [isPromotingVersion, setIsPromotingVersion] = useState(false); const [stableHtml, setStableHtml] = useState(""); const [throttledHtml, setThrottledHtml] = useState(""); const lastUpdateTimeRef = useRef(0); // For new projects, throttle HTML updates to every 3 seconds useEffect(() => { if (isNew && currentPageData?.html) { const now = Date.now(); const timeSinceLastUpdate = now - lastUpdateTimeRef.current; // If this is the first update or 3 seconds have passed, update immediately if (lastUpdateTimeRef.current === 0 || timeSinceLastUpdate >= 3000) { setThrottledHtml(currentPageData.html); lastUpdateTimeRef.current = now; } else { // Otherwise, schedule an update for when 3 seconds will have passed const timeUntilNextUpdate = 3000 - timeSinceLastUpdate; const timer = setTimeout(() => { setThrottledHtml(currentPageData.html); lastUpdateTimeRef.current = Date.now(); }, timeUntilNextUpdate); return () => clearTimeout(timer); } } }, [isNew, currentPageData?.html]); useEffect(() => { if (!isAiWorking && !globalAiLoading && currentPageData?.html) { setStableHtml(currentPageData.html); } }, [isAiWorking, globalAiLoading, currentPageData?.html]); useEffect(() => { if ( currentPageData?.html && !stableHtml && !isAiWorking && !globalAiLoading ) { setStableHtml(currentPageData.html); } }, [currentPageData?.html, stableHtml, isAiWorking, globalAiLoading]); useUpdateEffect(() => { const cleanupListeners = () => { if (iframeRef?.current?.contentDocument) { const iframeDocument = iframeRef.current.contentDocument; iframeDocument.removeEventListener("mouseover", handleMouseOver); iframeDocument.removeEventListener("mouseout", handleMouseOut); iframeDocument.removeEventListener("click", handleClick); } }; if (iframeRef?.current) { const iframeDocument = iframeRef.current.contentDocument; if (iframeDocument) { cleanupListeners(); if (isEditableModeEnabled) { iframeDocument.addEventListener("mouseover", handleMouseOver); iframeDocument.addEventListener("mouseout", handleMouseOut); iframeDocument.addEventListener("click", handleClick); } } } return cleanupListeners; }, [iframeRef, isEditableModeEnabled]); const promoteVersion = async () => { setIsPromotingVersion(true); await api .post( `/me/projects/${project?.space_id}/commits/${currentCommit}/promote` ) .then((res) => { if (res.data.ok) { setCurrentCommit(null); setPages(res.data.pages); setCurrentPage(res.data.pages[0].path); toast.success("Version promoted successfully"); } }) .catch((err) => { toast.error(err.response.data.error); }); setIsPromotingVersion(false); }; const handleMouseOver = (event: MouseEvent) => { if (iframeRef?.current) { const iframeDocument = iframeRef.current.contentDocument; if (iframeDocument) { const targetElement = event.target as HTMLElement; if ( hoveredElement?.tagName !== targetElement.tagName || hoveredElement?.rect.top !== targetElement.getBoundingClientRect().top || hoveredElement?.rect.left !== targetElement.getBoundingClientRect().left || hoveredElement?.rect.width !== targetElement.getBoundingClientRect().width || hoveredElement?.rect.height !== targetElement.getBoundingClientRect().height ) { if (targetElement !== iframeDocument.body) { const rect = targetElement.getBoundingClientRect(); setHoveredElement({ tagName: targetElement.tagName, rect: { top: rect.top, left: rect.left, width: rect.width, height: rect.height, }, }); targetElement.classList.add("hovered-element"); } else { return setHoveredElement(null); } } } } }; const handleMouseOut = () => { setHoveredElement(null); }; const handleClick = (event: MouseEvent) => { if (iframeRef?.current) { const iframeDocument = iframeRef.current.contentDocument; if (iframeDocument) { const targetElement = event.target as HTMLElement; if (targetElement !== iframeDocument.body) { setSelectedElement(targetElement); } } } }; const handleCustomNavigation = (event: MouseEvent) => { if (iframeRef?.current) { const iframeDocument = iframeRef.current.contentDocument; if (iframeDocument) { const findClosestAnchor = ( element: HTMLElement ): HTMLAnchorElement | null => { let current = element; while (current && current !== iframeDocument.body) { if (current.tagName === "A") { return current as HTMLAnchorElement; } current = current.parentElement as HTMLElement; } return null; }; const anchorElement = findClosestAnchor(event.target as HTMLElement); if (anchorElement) { let href = anchorElement.getAttribute("href"); if (href) { event.stopPropagation(); event.preventDefault(); if (href.includes("#") && !href.includes(".html")) { const targetElement = iframeDocument.querySelector(href); if (targetElement) { targetElement.scrollIntoView({ behavior: "smooth" }); } return; } href = href.split(".html")[0] + ".html"; const isPageExist = pages.some((page) => page.path === href); if (isPageExist) { setCurrentPage(href); } } } } } }; return (
{!isAiWorking && hoveredElement && isEditableModeEnabled && (
{htmlTagToText(hoveredElement.tagName.toLowerCase())}
)} {isLoadingProject ? (
) : ( <>