| | 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 DeployButton from "./deploy-button/deploy-button"; |
| | import { defaultHTML } from "./../../utils/consts"; |
| | import Tabs from "./tabs/tabs"; |
| | import AskAI from "./ask-ai/ask-ai"; |
| | import { Auth } from "./../../utils/types"; |
| | 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 [error, setError] = useState(false); |
| | const [html, setHtml] = useState((htmlStorage as string) ?? defaultHTML); |
| | const [isAiWorking, setisAiWorking] = useState(false); |
| | const [auth, setAuth] = useState<Auth | undefined>(undefined); |
| | const [currentView, setCurrentView] = useState<"editor" | "preview">( |
| | "editor" |
| | ); |
| |
|
| | const fetchMe = async () => { |
| | const res = await fetch("/api/@me"); |
| | if (res.ok) { |
| | const data = await res.json(); |
| | setAuth(data); |
| | } else { |
| | setAuth(undefined); |
| | } |
| | }; |
| |
|
| | 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(() => { |
| | |
| | fetchMe(); |
| | 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); |
| | setError(false); |
| | removeHtmlStorage(); |
| | editorRef.current?.revealLine( |
| | editorRef.current?.getModel()?.getLineCount() ?? 0 |
| | ); |
| | } |
| | }} |
| | > |
| | <DeployButton html={html} error={error} auth={auth} /> |
| | </Header> |
| | <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} |
| | onValidate={(markers) => { |
| | if (markers?.length > 0) { |
| | setError(true); |
| | } |
| | }} |
| | onChange={(value) => { |
| | const newValue = value ?? ""; |
| | setHtml(newValue); |
| | setError(false); |
| | }} |
| | onMount={(editor) => (editorRef.current = editor)} |
| | /> |
| | </div> |
| | <AskAI |
| | html={html} |
| | setHtml={setHtml} |
| | isAiWorking={isAiWorking} |
| | setisAiWorking={setisAiWorking} |
| | setView={setCurrentView} |
| | 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; |
| |
|