eddyencode/deepsitev3
1
1import { useRef, useState } from "react";2import Editor from "@monaco-editor/react";3import classNames from "classnames";4import { editor } from "monaco-editor";5import {6 useMount,7 useUnmount,8 useEvent,9 useLocalStorage,10 useSearchParam,11} from "react-use";12import { toast } from "react-toastify";13 14import Header from "./header/header";15import DeployButton from "./deploy-button/deploy-button";16import { defaultHTML } from "./../../utils/consts";17import Tabs from "./tabs/tabs";18import AskAI from "./ask-ai/ask-ai";19import { Auth } from "./../../utils/types";20import Preview from "./preview/preview";21import LoadButton from "./load-button/load-button";22 23function App() {24 const [htmlStorage, , removeHtmlStorage] = useLocalStorage("html_content");25 const remix = useSearchParam("remix");26 27 const preview = useRef<HTMLDivElement>(null);28 const editor = useRef<HTMLDivElement>(null);29 const resizer = useRef<HTMLDivElement>(null);30 const editorRef = useRef<editor.IStandaloneCodeEditor | null>(null);31 32 const [isResizing, setIsResizing] = useState(false);33 const [error, setError] = useState(false);34 const [html, setHtml] = useState((htmlStorage as string) ?? defaultHTML);35 const [isAiWorking, setisAiWorking] = useState(false);36 const [auth, setAuth] = useState<Auth | undefined>(undefined);37 const [currentView, setCurrentView] = useState<"editor" | "preview">(38 "editor"39 );40 41 const fetchMe = async () => {42 const res = await fetch("/api/@me");43 if (res.ok) {44 const data = await res.json();45 setAuth(data);46 } else {47 setAuth(undefined);48 }49 };50 51 const fetchRemix = async () => {52 if (!remix) return;53 const res = await fetch(`/api/remix/${remix}`);54 if (res.ok) {55 const data = await res.json();56 if (data.html) {57 setHtml(data.html);58 toast.success("Remix content loaded successfully.");59 }60 } else {61 toast.error("Failed to load remix content.");62 }63 const url = new URL(window.location.href);64 url.searchParams.delete("remix");65 window.history.replaceState({}, document.title, url.toString());66 };67 68 /**69 * Resets the layout based on screen size70 * - For desktop: Sets editor to 1/3 width and preview to 2/371 * - For mobile: Removes inline styles to let CSS handle it72 */73 const resetLayout = () => {74 if (!editor.current || !preview.current) return;75 76 // lg breakpoint is 1024px based on useBreakpoint definition and Tailwind defaults77 if (window.innerWidth >= 1024) {78 // Set initial 1/3 - 2/3 sizes for large screens, accounting for resizer width79 const resizerWidth = resizer.current?.offsetWidth ?? 8; // w-2 = 0.5rem = 8px80 const availableWidth = window.innerWidth - resizerWidth;81 const initialEditorWidth = availableWidth / 3; // Editor takes 1/3 of space82 const initialPreviewWidth = availableWidth - initialEditorWidth; // Preview takes 2/383 editor.current.style.width = `${initialEditorWidth}px`;84 preview.current.style.width = `${initialPreviewWidth}px`;85 } else {86 // Remove inline styles for smaller screens, let CSS flex-col handle it87 editor.current.style.width = "";88 preview.current.style.width = "";89 }90 };91 92 /**93 * Handles resizing when the user drags the resizer94 * Ensures minimum widths are maintained for both panels95 */96 const handleResize = (e: MouseEvent) => {97 if (!editor.current || !preview.current || !resizer.current) return;98 99 const resizerWidth = resizer.current.offsetWidth;100 const minWidth = 100; // Minimum width for editor/preview101 const maxWidth = window.innerWidth - resizerWidth - minWidth;102 103 const editorWidth = e.clientX;104 const clampedEditorWidth = Math.max(105 minWidth,106 Math.min(editorWidth, maxWidth)107 );108 const calculatedPreviewWidth =109 window.innerWidth - clampedEditorWidth - resizerWidth;110 111 editor.current.style.width = `${clampedEditorWidth}px`;112 preview.current.style.width = `${calculatedPreviewWidth}px`;113 };114 115 const handleMouseDown = () => {116 setIsResizing(true);117 document.addEventListener("mousemove", handleResize);118 document.addEventListener("mouseup", handleMouseUp);119 };120 121 const handleMouseUp = () => {122 setIsResizing(false);123 document.removeEventListener("mousemove", handleResize);124 document.removeEventListener("mouseup", handleMouseUp);125 };126 127 // Prevent accidental navigation away when AI is working or content has changed128 useEvent("beforeunload", (e) => {129 if (isAiWorking || html !== defaultHTML) {130 e.preventDefault();131 return "";132 }133 });134 135 // Initialize component on mount136 useMount(() => {137 // Fetch user data138 fetchMe();139 fetchRemix();140 141 // Restore content from storage if available142 if (htmlStorage) {143 removeHtmlStorage();144 toast.warn("Previous HTML content restored from local storage.");145 }146 147 // Set initial layout based on window size148 resetLayout();149 150 // Attach event listeners151 if (!resizer.current) return;152 resizer.current.addEventListener("mousedown", handleMouseDown);153 window.addEventListener("resize", resetLayout);154 });155 156 // Clean up event listeners on unmount157 useUnmount(() => {158 document.removeEventListener("mousemove", handleResize);159 document.removeEventListener("mouseup", handleMouseUp);160 if (resizer.current) {161 resizer.current.removeEventListener("mousedown", handleMouseDown);162 }163 window.removeEventListener("resize", resetLayout);164 });165 166 return (167 <div className="h-screen bg-gray-950 font-sans overflow-hidden">168 <Header169 onReset={() => {170 if (isAiWorking) {171 toast.warn("Please wait for the AI to finish working.");172 return;173 }174 if (175 window.confirm("You're about to reset the editor. Are you sure?")176 ) {177 setHtml(defaultHTML);178 setError(false);179 removeHtmlStorage();180 editorRef.current?.revealLine(181 editorRef.current?.getModel()?.getLineCount() ?? 0182 );183 }184 }}185 >186 <div className="flex items-center justify-end gap-5">187 <LoadButton auth={auth} setHtml={setHtml} />188 <DeployButton html={html} error={error} auth={auth} />189 </div>190 </Header>191 <main className="max-lg:flex-col flex w-full">192 <div193 ref={editor}194 className={classNames(195 "w-full h-[calc(100dvh-49px)] lg:h-[calc(100dvh-54px)] relative overflow-hidden max-lg:transition-all max-lg:duration-200 select-none",196 {197 "max-lg:h-0": currentView === "preview",198 }199 )}200 >201 <Tabs />202 <div203 onClick={(e) => {204 if (isAiWorking) {205 e.preventDefault();206 e.stopPropagation();207 toast.warn("Please wait for the AI to finish working.");208 }209 }}210 >211 <Editor212 language="html"213 theme="vs-dark"214 className={classNames(215 "h-[calc(100dvh-90px)] lg:h-[calc(100dvh-96px)]",216 {217 "pointer-events-none": isAiWorking,218 }219 )}220 value={html}221 onValidate={(markers) => {222 if (markers?.length > 0) {223 setError(true);224 }225 }}226 onChange={(value) => {227 const newValue = value ?? "";228 setHtml(newValue);229 setError(false);230 }}231 onMount={(editor) => (editorRef.current = editor)}232 />233 </div>234 <AskAI235 html={html}236 setHtml={setHtml}237 isAiWorking={isAiWorking}238 setisAiWorking={setisAiWorking}239 setView={setCurrentView}240 onScrollToBottom={() => {241 editorRef.current?.revealLine(242 editorRef.current?.getModel()?.getLineCount() ?? 0243 );244 }}245 />246 </div>247 <div248 ref={resizer}249 className="bg-gray-700 hover:bg-blue-500 w-2 cursor-col-resize h-[calc(100dvh-53px)] max-lg:hidden"250 />251 <Preview252 html={html}253 isResizing={isResizing}254 isAiWorking={isAiWorking}255 ref={preview}256 setView={setCurrentView}257 />258 </main>259 </div>260 );261}262 263export default App;264 