enzostvs's picture
enzostvs HF Staff
fix mobile
7734a90
"use client";
import { useRef, useState, useEffect, forwardRef } 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 { Button } from "@/components/ui/button";
import { LivePreview, LivePreviewRef } from "../live-preview";
import { HistoryNotification } from "../history-notification";
import { AlertCircle } from "lucide-react";
import { api } from "@/lib/api";
import { toast } from "sonner";
import Loading from "@/components/loading";
export const Preview = forwardRef<LivePreviewRef, { isNew: boolean }>(
({ isNew }, ref) => {
const {
project,
device,
isLoadingProject,
currentTab,
currentCommit,
setCurrentCommit,
currentPageData,
pages,
setPages,
setCurrentPage,
isSameHtml,
} = useEditor();
const {
isEditableModeEnabled,
setSelectedElement,
isAiWorking,
globalAiLoading,
} = useAi();
const iframeRef = useRef<HTMLIFrameElement>(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<string>("");
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 (
<div
className={classNames(
"bg-neutral-900/30 w-full h-[calc(100dvh-57px)] flex flex-col items-center justify-center relative z-1 lg:border-l border-neutral-800",
{
"max-lg:h-0 overflow-hidden": currentTab === "chat",
"max-lg:h-full": currentTab === "preview",
}
)}
>
<GridPattern
x={-1}
y={-1}
strokeDasharray={"4 2"}
className={cn(
"[mask-image:radial-gradient(900px_circle_at_center,white,transparent)] opacity-40"
)}
/>
{!isAiWorking && hoveredElement && isEditableModeEnabled && (
<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: hoveredElement.rect.top,
left: hoveredElement.rect.left,
width: hoveredElement.rect.width,
height: hoveredElement.rect.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(hoveredElement.tagName.toLowerCase())}
</span>
</div>
)}
{isNew && !isLoadingProject && !globalAiLoading && isSameHtml ? (
<iframe
className={classNames(
"w-full select-none transition-all duration-200 bg-black h-full",
{
"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",
}
)}
srcDoc={defaultHTML}
/>
) : (isNew && globalAiLoading) || isLoadingProject ? (
<div className="w-full h-full flex items-center justify-center relative">
<div className="py-10 w-full relative z-1 max-w-3xl mx-auto text-center">
<AiLoading
text={isLoadingProject ? "Fetching your project..." : undefined}
className="flex-col"
/>
<AnimatedBlobs />
<AnimatedBlobs />
</div>
{!isLoadingProject && (
<LivePreview
ref={ref}
currentPageData={currentPageData}
isAiWorking={isAiWorking}
defaultHTML={defaultHTML}
className="bottom-4 left-4"
/>
)}
</div>
) : (
<>
<iframe
id="preview-iframe"
ref={iframeRef}
className={classNames(
"w-full select-none transition-all duration-200 bg-black h-full",
{
"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",
}
)}
src={
currentCommit
? `https://${project?.space_id?.replaceAll(
"/",
"-"
)}--rev-${currentCommit.slice(0, 7)}.static.hf.space`
: undefined
}
srcDoc={!currentCommit ? stableHtml : undefined}
onLoad={
!currentCommit
? () => {
if (iframeRef?.current?.contentWindow?.document?.body) {
iframeRef.current.contentWindow.document.body.scrollIntoView(
{
block: isAiWorking ? "end" : "start",
inline: "nearest",
behavior: isAiWorking ? "instant" : "smooth",
}
);
}
// add event listener to all links in the iframe to handle navigation
if (iframeRef?.current?.contentWindow?.document) {
const links =
iframeRef.current.contentWindow.document.querySelectorAll(
"a"
);
links.forEach((link) => {
link.addEventListener(
"click",
handleCustomNavigation
);
});
}
}
: undefined
}
sandbox="allow-scripts allow-same-origin allow-popups allow-popups-to-escape-sandbox"
allow="accelerometer; ambient-light-sensor; autoplay; battery; camera; clipboard-read; clipboard-write; display-capture; document-domain; encrypted-media; fullscreen; geolocation; gyroscope; layout-animations; legacy-image-formats; magnetometer; microphone; midi; oversized-images; payment; picture-in-picture; publickey-credentials-get; serial; sync-xhr; usb; vr ; wake-lock; xr-spatial-tracking"
/>
<div
className={classNames(
"w-full h-full flex items-center justify-center absolute left-0 top-0 bg-black/40 backdrop-blur-lg transition-all duration-200",
{
"opacity-0 pointer-events-none": !globalAiLoading,
}
)}
>
<div className="py-10 w-full relative z-1 max-w-3xl mx-auto text-center">
<AiLoading
text={
isLoadingProject ? "Fetching your project..." : undefined
}
className="flex-col"
/>
<AnimatedBlobs />
<AnimatedBlobs />
</div>
</div>
<HistoryNotification
isVisible={!!currentCommit}
isPromotingVersion={isPromotingVersion}
onPromoteVersion={promoteVersion}
onGoBackToCurrent={() => setCurrentCommit(null)}
/>
</>
)}
</div>
);
}
);
Preview.displayName = "Preview";