File size: 5,005 Bytes
9c17740 7722a19 9c17740 7722a19 9c17740 7722a19 9c17740 7722a19 9c17740 7722a19 9c17740 7722a19 9c17740 7722a19 9c17740 7722a19 9c17740 7722a19 9c17740 7722a19 9c17740 7722a19 9c17740 7722a19 9c17740 7722a19 9c17740 7722a19 7734a90 7722a19 9c17740 7722a19 9c17740 7722a19 9c17740 7722a19 |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 |
"use client";
import {
useState,
useEffect,
useRef,
forwardRef,
useImperativeHandle,
} from "react";
import classNames from "classnames";
import { Button } from "@/components/ui/button";
import { Maximize, Minimize } from "lucide-react";
interface LivePreviewProps {
currentPageData: { path: string; html: string } | undefined;
isAiWorking: boolean;
defaultHTML: string;
className?: string;
}
export interface LivePreviewRef {
reset: () => void;
}
export const LivePreview = forwardRef<LivePreviewRef, LivePreviewProps>(
({ currentPageData, isAiWorking, defaultHTML, className }, ref) => {
const [isMaximized, setIsMaximized] = useState(false);
const [displayedHtml, setDisplayedHtml] = useState<string>("");
const latestHtmlRef = useRef<string>("");
const displayedHtmlRef = useRef<string>("");
const intervalRef = useRef<NodeJS.Timeout | null>(null);
const reset = () => {
setIsMaximized(false);
setDisplayedHtml("");
latestHtmlRef.current = "";
displayedHtmlRef.current = "";
if (intervalRef.current) {
clearInterval(intervalRef.current);
intervalRef.current = null;
}
};
useImperativeHandle(ref, () => ({
reset,
}));
useEffect(() => {
displayedHtmlRef.current = displayedHtml;
}, [displayedHtml]);
useEffect(() => {
if (currentPageData?.html && currentPageData.html !== defaultHTML) {
latestHtmlRef.current = currentPageData.html;
}
}, [currentPageData?.html, defaultHTML]);
useEffect(() => {
if (!currentPageData?.html || currentPageData.html === defaultHTML) {
return;
}
if (!displayedHtml || !isAiWorking) {
setDisplayedHtml(currentPageData.html);
if (intervalRef.current) {
clearInterval(intervalRef.current);
intervalRef.current = null;
}
return;
}
if (isAiWorking && !intervalRef.current) {
intervalRef.current = setInterval(() => {
if (
latestHtmlRef.current &&
latestHtmlRef.current !== displayedHtmlRef.current
) {
setDisplayedHtml(latestHtmlRef.current);
}
}, 3000);
}
}, [currentPageData?.html, defaultHTML, isAiWorking, displayedHtml]);
useEffect(() => {
if (!isAiWorking && intervalRef.current) {
clearInterval(intervalRef.current);
intervalRef.current = null;
if (latestHtmlRef.current) {
setDisplayedHtml(latestHtmlRef.current);
}
}
}, [isAiWorking]);
useEffect(() => {
return () => {
if (intervalRef.current) {
clearInterval(intervalRef.current);
intervalRef.current = null;
}
};
}, []);
if (!displayedHtml) {
return null;
}
return (
<div
className={classNames(
"absolute z-40 bg-white/95 backdrop-blur-sm border border-neutral-200 shadow-lg transition-all duration-500 ease-out transform scale-100 opacity-100 animate-in slide-in-from-bottom-4 zoom-in-95 rounded-xl",
{
"shadow-green-500/20 shadow-2xl border-green-200": isAiWorking,
},
className
)}
>
<div
className={classNames(
"flex flex-col animate-in fade-in duration-300",
isMaximized ? "w-[90dvw] lg:w-[60dvw] h-[80dvh]" : "w-80 h-96"
)}
>
<div className="flex items-center justify-between p-3 border-b border-neutral-200">
<div className="flex items-center gap-2">
<div className="size-2 bg-green-500 rounded-full animate-pulse shadow-sm shadow-green-500/50"></div>
<span className="text-xs font-medium text-neutral-800">
Live Preview
</span>
{isAiWorking && (
<span className="text-xs text-green-600 font-medium animate-pulse">
• Updating
</span>
)}
</div>
<div className="flex items-center gap-1">
<Button
variant="outline"
size="iconXs"
className="!rounded-md !border-neutral-200 hover:bg-neutral-50"
onClick={() => setIsMaximized(!isMaximized)}
>
{isMaximized ? (
<Minimize className="text-neutral-400 size-3" />
) : (
<Maximize className="text-neutral-400 size-3" />
)}
</Button>
</div>
</div>
<div className="flex-1 bg-black overflow-hidden relative rounded-b-xl">
<iframe
className="w-full h-full border-0"
srcDoc={displayedHtml}
sandbox="allow-scripts allow-same-origin"
title="Live Preview"
/>
</div>
</div>
</div>
);
}
);
LivePreview.displayName = "LivePreview";
|