| import { useRef, useState } from "react";
|
| import Editor from "@monaco-editor/react";
|
| import classNames from "classnames";
|
| import { editor } from "monaco-editor";
|
| import {
|
| useMount,
|
| useUnmount,
|
| useEvent,
|
| useLocalStorage,
|
| useSearchParam,
|
| } from "react-use";
|
| import { toast } from "react-toastify";
|
|
|
| import Header from "./header/header";
|
| import { defaultHTML } from "./../../utils/consts";
|
| import Tabs from "./tabs/tabs";
|
| import AskAI from "./ask-ai/ask-ai";
|
| import Preview from "./preview/preview";
|
|
|
| function App() {
|
| const [htmlStorage, , removeHtmlStorage] = useLocalStorage("html_content");
|
| const remix = useSearchParam("remix");
|
|
|
| const preview = useRef<HTMLDivElement>(null);
|
| const editor = useRef<HTMLDivElement>(null);
|
| const resizer = useRef<HTMLDivElement>(null);
|
| const editorRef = useRef<editor.IStandaloneCodeEditor | null>(null);
|
|
|
| const [isResizing, setIsResizing] = useState(false);
|
| const [html, setHtml] = useState((htmlStorage as string) ?? defaultHTML);
|
| const [isAiWorking, setisAiWorking] = useState(false);
|
| const [currentView, setCurrentView] = useState<"editor" | "preview">(
|
| "editor"
|
| );
|
|
|
| const fetchRemix = async () => {
|
| if (!remix) return;
|
| const res = await fetch(`/api/remix/${remix}`);
|
| if (res.ok) {
|
| const data = await res.json();
|
| if (data.html) {
|
| setHtml(data.html);
|
| toast.success("Remix content loaded successfully.");
|
| }
|
| } else {
|
| toast.error("Failed to load remix content.");
|
| }
|
| const url = new URL(window.location.href);
|
| url.searchParams.delete("remix");
|
| window.history.replaceState({}, document.title, url.toString());
|
| };
|
|
|
| |
| |
| |
| |
|
|
| const resetLayout = () => {
|
| if (!editor.current || !preview.current) return;
|
|
|
|
|
| if (window.innerWidth >= 1024) {
|
|
|
| const resizerWidth = resizer.current?.offsetWidth ?? 8;
|
| const availableWidth = window.innerWidth - resizerWidth;
|
| const initialEditorWidth = availableWidth / 3;
|
| const initialPreviewWidth = availableWidth - initialEditorWidth;
|
| editor.current.style.width = `${initialEditorWidth}px`;
|
| preview.current.style.width = `${initialPreviewWidth}px`;
|
| } else {
|
|
|
| editor.current.style.width = "";
|
| preview.current.style.width = "";
|
| }
|
| };
|
|
|
| |
| |
| |
|
|
| const handleResize = (e: MouseEvent) => {
|
| if (!editor.current || !preview.current || !resizer.current) return;
|
|
|
| const resizerWidth = resizer.current.offsetWidth;
|
| const minWidth = 100;
|
| const maxWidth = window.innerWidth - resizerWidth - minWidth;
|
|
|
| const editorWidth = e.clientX;
|
| const clampedEditorWidth = Math.max(
|
| minWidth,
|
| Math.min(editorWidth, maxWidth)
|
| );
|
| const calculatedPreviewWidth =
|
| window.innerWidth - clampedEditorWidth - resizerWidth;
|
|
|
| editor.current.style.width = `${clampedEditorWidth}px`;
|
| preview.current.style.width = `${calculatedPreviewWidth}px`;
|
| };
|
|
|
| const handleMouseDown = () => {
|
| setIsResizing(true);
|
| document.addEventListener("mousemove", handleResize);
|
| document.addEventListener("mouseup", handleMouseUp);
|
| };
|
|
|
| const handleMouseUp = () => {
|
| setIsResizing(false);
|
| document.removeEventListener("mousemove", handleResize);
|
| document.removeEventListener("mouseup", handleMouseUp);
|
| };
|
|
|
|
|
| useEvent("beforeunload", (e) => {
|
| if (isAiWorking || html !== defaultHTML) {
|
| e.preventDefault();
|
| return "";
|
| }
|
| });
|
|
|
|
|
| useMount(() => {
|
| fetchRemix();
|
|
|
|
|
| if (htmlStorage) {
|
| removeHtmlStorage();
|
| toast.warn("Previous HTML content restored from local storage.");
|
| }
|
|
|
|
|
| resetLayout();
|
|
|
|
|
| if (!resizer.current) return;
|
| resizer.current.addEventListener("mousedown", handleMouseDown);
|
| window.addEventListener("resize", resetLayout);
|
| });
|
|
|
|
|
| useUnmount(() => {
|
| document.removeEventListener("mousemove", handleResize);
|
| document.removeEventListener("mouseup", handleMouseUp);
|
| if (resizer.current) {
|
| resizer.current.removeEventListener("mousedown", handleMouseDown);
|
| }
|
| window.removeEventListener("resize", resetLayout);
|
| });
|
|
|
| return (
|
| <div className="h-screen bg-gray-950 font-sans overflow-hidden">
|
| <Header
|
| onReset={() => {
|
| if (isAiWorking) {
|
| toast.warn("Please wait for the AI to finish working.");
|
| return;
|
| }
|
| if (
|
| window.confirm("You're about to reset the editor. Are you sure?")
|
| ) {
|
| setHtml(defaultHTML);
|
| removeHtmlStorage();
|
| editorRef.current?.revealLine(
|
| editorRef.current?.getModel()?.getLineCount() ?? 0
|
| );
|
| }
|
| }}
|
| />
|
|
|
| <main className="max-lg:flex-col flex w-full">
|
| <div
|
| ref={editor}
|
| className={classNames(
|
| "w-full h-[calc(100dvh-49px)] lg:h-[calc(100dvh-54px)] relative overflow-hidden max-lg:transition-all max-lg:duration-200 select-none",
|
| {
|
| "max-lg:h-0": currentView === "preview",
|
| }
|
| )}
|
| >
|
| <Tabs />
|
| <div
|
| onClick={(e) => {
|
| if (isAiWorking) {
|
| e.preventDefault();
|
| e.stopPropagation();
|
| toast.warn("Please wait for the AI to finish working.");
|
| }
|
| }}
|
| >
|
| <Editor
|
| language="html"
|
| theme="vs-dark"
|
| className={classNames(
|
| "h-[calc(100dvh-90px)] lg:h-[calc(100dvh-96px)]",
|
| {
|
| "pointer-events-none": isAiWorking,
|
| }
|
| )}
|
| value={html}
|
| onChange={(value) => {
|
| const newValue = value ?? "";
|
| setHtml(newValue);
|
| }}
|
| onMount={(editor) => (editorRef.current = editor)}
|
| />
|
| </div>
|
| <AskAI
|
| html={html}
|
| setHtml={setHtml}
|
| isAiWorking={isAiWorking}
|
| setisAiWorking={setisAiWorking}
|
| setView={setCurrentView}
|
| onNewPrompt={() => {}}
|
| onScrollToBottom={() => {
|
| editorRef.current?.revealLine(
|
| editorRef.current?.getModel()?.getLineCount() ?? 0
|
| );
|
| }}
|
| />
|
| </div>
|
| <div
|
| ref={resizer}
|
| className="bg-gray-700 hover:bg-blue-500 w-2 cursor-col-resize h-[calc(100dvh-53px)] max-lg:hidden"
|
| />
|
| <Preview
|
| html={html}
|
| isResizing={isResizing}
|
| isAiWorking={isAiWorking}
|
| ref={preview}
|
| setView={setCurrentView}
|
| />
|
| </main>
|
| </div>
|
| );
|
| }
|
|
|
| export default App;
|
|
|