CoolFace
Apppublic

Exched/DeepSeek_Coder

sourceHugging Facemitupdated 11mo agoView on Hugging Face
0likes
index.tsx359 linesDownload Raw Back to preview
1"use client";2 3import { useRef, useState, useEffect } from "react";4import { useUpdateEffect } from "react-use";5import classNames from "classnames";6 7import { cn } from "@/lib/utils";8import { GridPattern } from "@/components/magic-ui/grid-pattern";9import { useEditor } from "@/hooks/useEditor";10import { useAi } from "@/hooks/useAi";11import { htmlTagToText } from "@/lib/html-tag-to-text";12import { AnimatedBlobs } from "@/components/animated-blobs";13import { AiLoading } from "../ask-ai/loading";14import { defaultHTML } from "@/lib/consts";15import { HistoryNotification } from "../history-notification";16import { api } from "@/lib/api";17import { toast } from "sonner";18 19export const Preview = ({ isNew }: { isNew: boolean }) => {20  const {21    project,22    device,23    isLoadingProject,24    currentTab,25    currentCommit,26    setCurrentCommit,27    currentPageData,28    pages,29    setPages,30    setCurrentPage,31    isSameHtml,32  } = useEditor();33  const {34    isEditableModeEnabled,35    setSelectedElement,36    isAiWorking,37    globalAiLoading,38  } = useAi();39 40  const iframeRef = useRef<HTMLIFrameElement>(null);41 42  const [hoveredElement, setHoveredElement] = useState<{43    tagName: string;44    rect: { top: number; left: number; width: number; height: number };45  } | null>(null);46  const [isPromotingVersion, setIsPromotingVersion] = useState(false);47  const [stableHtml, setStableHtml] = useState<string>("");48  const [throttledHtml, setThrottledHtml] = useState<string>("");49  const lastUpdateTimeRef = useRef<number>(0);50 51  // For new projects, throttle HTML updates to every 3 seconds52  useEffect(() => {53    if (isNew && currentPageData?.html) {54      const now = Date.now();55      const timeSinceLastUpdate = now - lastUpdateTimeRef.current;56 57      // If this is the first update or 3 seconds have passed, update immediately58      if (lastUpdateTimeRef.current === 0 || timeSinceLastUpdate >= 3000) {59        setThrottledHtml(currentPageData.html);60        lastUpdateTimeRef.current = now;61      } else {62        // Otherwise, schedule an update for when 3 seconds will have passed63        const timeUntilNextUpdate = 3000 - timeSinceLastUpdate;64        const timer = setTimeout(() => {65          setThrottledHtml(currentPageData.html);66          lastUpdateTimeRef.current = Date.now();67        }, timeUntilNextUpdate);68        return () => clearTimeout(timer);69      }70    }71  }, [isNew, currentPageData?.html]);72 73  useEffect(() => {74    if (!isAiWorking && !globalAiLoading && currentPageData?.html) {75      setStableHtml(currentPageData.html);76    }77  }, [isAiWorking, globalAiLoading, currentPageData?.html]);78 79  useEffect(() => {80    if (81      currentPageData?.html &&82      !stableHtml &&83      !isAiWorking &&84      !globalAiLoading85    ) {86      setStableHtml(currentPageData.html);87    }88  }, [currentPageData?.html, stableHtml, isAiWorking, globalAiLoading]);89 90  useUpdateEffect(() => {91    const cleanupListeners = () => {92      if (iframeRef?.current?.contentDocument) {93        const iframeDocument = iframeRef.current.contentDocument;94        iframeDocument.removeEventListener("mouseover", handleMouseOver);95        iframeDocument.removeEventListener("mouseout", handleMouseOut);96        iframeDocument.removeEventListener("click", handleClick);97      }98    };99 100    if (iframeRef?.current) {101      const iframeDocument = iframeRef.current.contentDocument;102      if (iframeDocument) {103        cleanupListeners();104 105        if (isEditableModeEnabled) {106          iframeDocument.addEventListener("mouseover", handleMouseOver);107          iframeDocument.addEventListener("mouseout", handleMouseOut);108          iframeDocument.addEventListener("click", handleClick);109        }110      }111    }112 113    return cleanupListeners;114  }, [iframeRef, isEditableModeEnabled]);115 116  const promoteVersion = async () => {117    setIsPromotingVersion(true);118    await api119      .post(120        `/me/projects/${project?.space_id}/commits/${currentCommit}/promote`121      )122      .then((res) => {123        if (res.data.ok) {124          setCurrentCommit(null);125          setPages(res.data.pages);126          setCurrentPage(res.data.pages[0].path);127          toast.success("Version promoted successfully");128        }129      })130      .catch((err) => {131        toast.error(err.response.data.error);132      });133    setIsPromotingVersion(false);134  };135 136  const handleMouseOver = (event: MouseEvent) => {137    if (iframeRef?.current) {138      const iframeDocument = iframeRef.current.contentDocument;139      if (iframeDocument) {140        const targetElement = event.target as HTMLElement;141        if (142          hoveredElement?.tagName !== targetElement.tagName ||143          hoveredElement?.rect.top !==144            targetElement.getBoundingClientRect().top ||145          hoveredElement?.rect.left !==146            targetElement.getBoundingClientRect().left ||147          hoveredElement?.rect.width !==148            targetElement.getBoundingClientRect().width ||149          hoveredElement?.rect.height !==150            targetElement.getBoundingClientRect().height151        ) {152          if (targetElement !== iframeDocument.body) {153            const rect = targetElement.getBoundingClientRect();154            setHoveredElement({155              tagName: targetElement.tagName,156              rect: {157                top: rect.top,158                left: rect.left,159                width: rect.width,160                height: rect.height,161              },162            });163            targetElement.classList.add("hovered-element");164          } else {165            return setHoveredElement(null);166          }167        }168      }169    }170  };171  const handleMouseOut = () => {172    setHoveredElement(null);173  };174  const handleClick = (event: MouseEvent) => {175    if (iframeRef?.current) {176      const iframeDocument = iframeRef.current.contentDocument;177      if (iframeDocument) {178        const targetElement = event.target as HTMLElement;179        if (targetElement !== iframeDocument.body) {180          setSelectedElement(targetElement);181        }182      }183    }184  };185 186  const handleCustomNavigation = (event: MouseEvent) => {187    if (iframeRef?.current) {188      const iframeDocument = iframeRef.current.contentDocument;189      if (iframeDocument) {190        const findClosestAnchor = (191          element: HTMLElement192        ): HTMLAnchorElement | null => {193          let current = element;194          while (current && current !== iframeDocument.body) {195            if (current.tagName === "A") {196              return current as HTMLAnchorElement;197            }198            current = current.parentElement as HTMLElement;199          }200          return null;201        };202 203        const anchorElement = findClosestAnchor(event.target as HTMLElement);204        if (anchorElement) {205          let href = anchorElement.getAttribute("href");206          if (href) {207            event.stopPropagation();208            event.preventDefault();209 210            if (href.includes("#") && !href.includes(".html")) {211              const targetElement = iframeDocument.querySelector(href);212              if (targetElement) {213                targetElement.scrollIntoView({ behavior: "smooth" });214              }215              return;216            }217 218            href = href.split(".html")[0] + ".html";219            const isPageExist = pages.some((page) => page.path === href);220            if (isPageExist) {221              setCurrentPage(href);222            }223          }224        }225      }226    }227  };228 229  return (230    <div231      className={classNames(232        "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",233        {234          "max-lg:h-0 overflow-hidden": currentTab === "chat",235          "max-lg:h-full": currentTab === "preview",236        }237      )}238    >239      <GridPattern240        x={-1}241        y={-1}242        strokeDasharray={"4 2"}243        className={cn(244          "[mask-image:radial-gradient(900px_circle_at_center,white,transparent)] opacity-40"245        )}246      />247      {!isAiWorking && hoveredElement && isEditableModeEnabled && (248        <div249          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"250          style={{251            top: hoveredElement.rect.top,252            left: hoveredElement.rect.left,253            width: hoveredElement.rect.width,254            height: hoveredElement.rect.height,255          }}256        >257          <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">258            {htmlTagToText(hoveredElement.tagName.toLowerCase())}259          </span>260        </div>261      )}262      {isLoadingProject ? (263        <div className="w-full h-full flex items-center justify-center relative">264          <div className="py-10 w-full relative z-1 max-w-3xl mx-auto text-center">265            <AiLoading text="Fetching your project..." className="flex-col" />266            <AnimatedBlobs />267            <AnimatedBlobs />268          </div>269        </div>270      ) : (271        <>272          <iframe273            id="preview-iframe"274            ref={iframeRef}275            className={classNames(276              "w-full select-none transition-all duration-200 bg-black h-full",277              {278                "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]":279                  device === "mobile",280              }281            )}282            src={283              currentCommit284                ? `https://${project?.space_id?.replaceAll(285                    "/",286                    "-"287                  )}--rev-${currentCommit.slice(0, 7)}.static.hf.space`288                : undefined289            }290            srcDoc={291              !currentCommit292                ? isNew293                  ? throttledHtml || defaultHTML294                  : stableHtml295                : undefined296            }297            onLoad={298              !currentCommit299                ? () => {300                    if (iframeRef?.current?.contentWindow?.document?.body) {301                      iframeRef.current.contentWindow.document.body.scrollIntoView(302                        {303                          block: isAiWorking ? "end" : "start",304                          inline: "nearest",305                          behavior: isAiWorking ? "instant" : "smooth",306                        }307                      );308                    }309                    // add event listener to all links in the iframe to handle navigation310                    if (iframeRef?.current?.contentWindow?.document) {311                      const links =312                        iframeRef.current.contentWindow.document.querySelectorAll(313                          "a"314                        );315                      links.forEach((link) => {316                        link.addEventListener("click", handleCustomNavigation);317                      });318                    }319                  }320                : undefined321            }322            sandbox="allow-scripts allow-same-origin allow-popups allow-popups-to-escape-sandbox"323            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"324          />325          {!isNew && (326            <>327              <div328                className={classNames(329                  "w-full h-full flex items-center justify-center absolute left-0 top-0 bg-black/40 backdrop-blur-lg transition-all duration-200",330                  {331                    "opacity-0 pointer-events-none": !globalAiLoading,332                  }333                )}334              >335                <div className="py-10 w-full relative z-1 max-w-3xl mx-auto text-center">336                  <AiLoading337                    text={338                      isLoadingProject ? "Fetching your project..." : undefined339                    }340                    className="flex-col"341                  />342                  <AnimatedBlobs />343                  <AnimatedBlobs />344                </div>345              </div>346              <HistoryNotification347                isVisible={!!currentCommit}348                isPromotingVersion={isPromotingVersion}349                onPromoteVersion={promoteVersion}350                onGoBackToCurrent={() => setCurrentCommit(null)}351              />352            </>353          )}354        </>355      )}356    </div>357  );358};359