|
"use client"; |
|
import { useUpdateEffect } from "react-use"; |
|
import { useMemo, useState } from "react"; |
|
import classNames from "classnames"; |
|
import { toast } from "sonner"; |
|
|
|
import { cn } from "@/lib/utils"; |
|
import { GridPattern } from "@/components/magic-ui/grid-pattern"; |
|
import { htmlTagToText } from "@/lib/html-tag-to-text"; |
|
|
|
export const Preview = ({ |
|
html, |
|
isResizing, |
|
isAiWorking, |
|
ref, |
|
device, |
|
currentTab, |
|
iframeRef, |
|
isEditableModeEnabled, |
|
onClickElement, |
|
}: { |
|
html: string; |
|
isResizing: boolean; |
|
isAiWorking: boolean; |
|
ref: React.RefObject<HTMLDivElement | null>; |
|
iframeRef?: React.RefObject<HTMLIFrameElement | null>; |
|
device: "desktop" | "mobile"; |
|
currentTab: string; |
|
isEditableModeEnabled?: boolean; |
|
onClickElement?: (element: HTMLElement) => void; |
|
}) => { |
|
const [hoveredElement, setHoveredElement] = useState<HTMLElement | null>( |
|
null |
|
); |
|
|
|
|
|
const handleMouseOver = (event: MouseEvent) => { |
|
if (iframeRef?.current) { |
|
const iframeDocument = iframeRef.current.contentDocument; |
|
if (iframeDocument) { |
|
const targetElement = event.target as HTMLElement; |
|
if ( |
|
hoveredElement !== targetElement && |
|
targetElement !== iframeDocument.body |
|
) { |
|
setHoveredElement(targetElement); |
|
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) { |
|
onClickElement?.(targetElement); |
|
} |
|
} |
|
} |
|
}; |
|
|
|
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 selectedElement = useMemo(() => { |
|
if (!isEditableModeEnabled) return null; |
|
if (!hoveredElement) return null; |
|
return hoveredElement; |
|
}, [hoveredElement, isEditableModeEnabled]); |
|
|
|
return ( |
|
<div |
|
ref={ref} |
|
className={classNames( |
|
"w-full border-l border-gray-900 h-full relative z-0 flex items-center justify-center", |
|
{ |
|
"lg:p-4": currentTab !== "preview", |
|
"max-lg:h-0": currentTab === "chat", |
|
"max-lg:h-full": currentTab === "preview", |
|
} |
|
)} |
|
onClick={(e) => { |
|
if (isAiWorking) { |
|
e.preventDefault(); |
|
e.stopPropagation(); |
|
toast.warning("Please wait for the AI to finish working."); |
|
} |
|
}} |
|
> |
|
<GridPattern |
|
x={-1} |
|
y={-1} |
|
strokeDasharray={"4 2"} |
|
className={cn( |
|
"[mask-image:radial-gradient(900px_circle_at_center,white,transparent)]" |
|
)} |
|
/> |
|
{!isAiWorking && hoveredElement && selectedElement && ( |
|
<div |
|
className="cursor-pointer absolute bg-sky-500/10 border-[2px] border-dashed border-sky-500 rounded-r-lg rounded-b-lg p-3 z-10 pointer-events-none" |
|
style={{ |
|
top: |
|
selectedElement.getBoundingClientRect().top + |
|
(currentTab === "preview" ? 0 : 24), |
|
left: |
|
selectedElement.getBoundingClientRect().left + |
|
(currentTab === "preview" ? 0 : 24), |
|
width: selectedElement.getBoundingClientRect().width, |
|
height: selectedElement.getBoundingClientRect().height, |
|
}} |
|
> |
|
<span className="bg-sky-500 rounded-t-md text-sm text-neutral-100 px-2 py-0.5 -translate-y-7 absolute top-0 left-0"> |
|
{htmlTagToText(selectedElement.tagName.toLowerCase())} |
|
</span> |
|
</div> |
|
)} |
|
<iframe |
|
id="preview-iframe" |
|
ref={iframeRef} |
|
title="output" |
|
className={classNames( |
|
"w-full select-none transition-all duration-200 bg-black h-full", |
|
{ |
|
"pointer-events-none": isResizing || isAiWorking, |
|
"lg:max-w-md lg:mx-auto lg:!rounded-[42px] lg:border-[8px] lg:border-neutral-700 lg:shadow-2xl lg:h-[80dvh] lg:max-h-[996px]": |
|
device === "mobile", |
|
"lg:border-[8px] lg:border-neutral-700 lg:shadow-2xl lg:rounded-[24px]": |
|
currentTab !== "preview" && device === "desktop", |
|
} |
|
)} |
|
srcDoc={html} |
|
onLoad={() => { |
|
if (iframeRef?.current?.contentWindow?.document?.body) { |
|
iframeRef.current.contentWindow.document.body.scrollIntoView({ |
|
block: isAiWorking ? "end" : "start", |
|
inline: "nearest", |
|
behavior: isAiWorking ? "instant" : "smooth", |
|
}); |
|
} |
|
}} |
|
/> |
|
</div> |
|
); |
|
}; |
|
|