diff --git a/app/components/CodeEditor/CodeEditor.tsx b/app/components/CodeEditor/CodeEditor.tsx index 5118db2..3857e1b 100644 --- a/app/components/CodeEditor/CodeEditor.tsx +++ b/app/components/CodeEditor/CodeEditor.tsx @@ -3,44 +3,20 @@ import styles from "./CodeEditor.module.css"; import ctx from "classnames"; import { GeistMono } from "geist/font/mono"; -import Editor from "@monaco-editor/react"; +import Editor, { Monaco } from "@monaco-editor/react"; import { Flex, useColorMode } from "@chakra-ui/react"; import { useEffect, useState, useRef } from "react"; import MyBtn from "../MyBtn"; -import { CodeFile, OutputResult } from "@/lib/types"; -import { OutputReducerAction } from "@/lib/reducers"; import { tryFormattingCode, validateCode } from "@/lib/client-functions"; import FiChevronRight from "@/app/styles/icons/HiChevronRightGreen"; import { useRouter } from "next/navigation"; import { useUserSolutionStore, useEditorStore } from "@/lib/stores"; import { sendGAEvent } from "@next/third-parties/google"; +import { CodeFile, OutputResult } from "@/lib/types"; +import { OutputReducerAction } from "@/lib/reducers"; -export default function CodeEditor({ - codeString, - setCodeString, - codeFile, - dispatchOutput, - nextStepPath, - stepIndex, - chapterIndex, - outputResult, -}: { - codeString: string; - setCodeString: (codeString: string) => void; - codeFile: CodeFile; - dispatchOutput: React.Dispatch; - nextStepPath: string | undefined; - stepIndex: number; - chapterIndex: number; - outputResult: OutputResult; -}) { - const { colorMode } = useColorMode(); - const [monaco, setMonaco] = useState(null); - const router = useRouter(); - const editorStore = useEditorStore(); - const userSolutionStore = useUserSolutionStore(); - const editorRef = useRef(null); - +// Custom hook for editor theme setup +const useEditorTheme = (monaco: Monaco, colorMode: "dark" | "light") => { useEffect(() => { if (monaco) { monaco.editor.defineTheme("my-theme", { @@ -54,31 +30,42 @@ export default function CodeEditor({ monaco.editor.setTheme(colorMode === "light" ? "light" : "my-theme"); } }, [monaco, colorMode]); +}; + +// Custom hook for keyboard shortcuts +const useValidationShortcut = ( + handleValidate: () => void, + codeString: string, +) => { useEffect(() => { const handleKeyDown = (event: KeyboardEvent) => { - if (event.key == "Enter" && event.shiftKey) { + if (event.key === "Enter" && event.shiftKey) { sendGAEvent("event", "buttonClicked", { value: "Validate (through shortcut)", }); event.preventDefault(); - tryFormattingCode(editorRef, setCodeString); - validateCode( - codeString, - codeFile, - dispatchOutput, - stepIndex, - chapterIndex, - ); + handleValidate(); } }; document.addEventListener("keydown", handleKeyDown); - return () => { document.removeEventListener("keydown", handleKeyDown); }; - }, [codeString]); + }, [handleValidate, codeString]); +}; + +// Custom hook for code persistence +const useCodePersistence = ( + chapterIndex: number, + stepIndex: number, + codeString: string, + setCodeString: (value: string) => void, + codeFile: CodeFile, +) => { + const userSolutionStore = useUserSolutionStore(); + // Load saved code useEffect(() => { const savedCode = userSolutionStore.getSavedUserSolutionByLesson( chapterIndex, @@ -89,6 +76,7 @@ export default function CodeEditor({ } }, [chapterIndex, stepIndex]); + // Save code changes useEffect(() => { userSolutionStore.saveUserSolutionForLesson( chapterIndex, @@ -97,11 +85,135 @@ export default function CodeEditor({ ); }, [codeString, chapterIndex, stepIndex]); + // Initialize code if no saved solutions useEffect(() => { - if (Object.keys(userSolutionStore.userSolutionsByLesson).length == 0) { + if (Object.keys(userSolutionStore.userSolutionsByLesson).length === 0) { setCodeString(JSON.stringify(codeFile.code, null, 2)); } }, [userSolutionStore]); +}; + +// EditorControls component for the buttons section +const EditorControls = ({ + handleValidate, + isValidating, + resetCode, + nextStepPath, + outputResult, +}: { + handleValidate: () => void; + isValidating: boolean; + resetCode: () => void; + nextStepPath: string | undefined; + outputResult: OutputResult; +}) => { + const router = useRouter(); + + return ( +
+ + + {isValidating ? "Validating ..." : "Validate"} + + + + Reset + + + { + if (nextStepPath) router.push("/" + nextStepPath); + }} + variant={ + outputResult.validityStatus === "valid" ? "default" : "success" + } + isDisabled={!nextStepPath} + size={outputResult.validityStatus === "valid" ? "sm" : "xs"} + > + Next + + +
+ ); +}; + +export default function CodeEditor({ + codeString, + setCodeString, + codeFile, + dispatchOutput, + nextStepPath, + stepIndex, + chapterIndex, + outputResult, +}: { + codeString: string; + setCodeString: (codeString: string) => void; + codeFile: CodeFile; + dispatchOutput: React.Dispatch; + nextStepPath: string | undefined; + stepIndex: number; + chapterIndex: number; + outputResult: OutputResult; +}) { + const { colorMode } = useColorMode(); + const [monaco, setMonaco] = useState(null); + const [isValidating, setIsValidating] = useState(false); + const editorStore = useEditorStore(); + const editorRef = useRef(null); + + // Apply custom hooks + useEditorTheme(monaco, colorMode); + + const handleValidate = () => { + setIsValidating(true); + setTimeout(() => { + tryFormattingCode(editorRef, setCodeString); + validateCode( + codeString, + codeFile, + dispatchOutput, + stepIndex, + chapterIndex, + ); + setIsValidating(false); + }, 500); + }; + + useValidationShortcut(handleValidate, codeString); + useCodePersistence( + chapterIndex, + stepIndex, + codeString, + setCodeString, + codeFile, + ); + + const resetCode = () => { + setCodeString(JSON.stringify(codeFile.code, null, 2)); + dispatchOutput({ type: "RESET" }); + }; + + const handleEditorMount = (editor: any, monaco: Monaco) => { + setMonaco(monaco); + + editorRef.current = editor; + editorStore.setEditor(editor); + editorStore.setMonaco(monaco); + }; return ( <> @@ -111,74 +223,24 @@ export default function CodeEditor({ defaultValue={codeString} theme={colorMode === "light" ? "light" : "my-theme"} value={codeString} - height={"100%"} + height="100%" onChange={(codeString) => setCodeString(codeString ?? "")} options={{ minimap: { enabled: false }, - fontSize: 14, formatOnPaste: true, formatOnType: true, }} - onMount={(editor, monaco) => { - setMonaco(monaco); - editorRef.current = editor; - editorStore.setEditor(editor); - editorStore.setMonaco(monaco); - }} + onMount={handleEditorMount} /> -
- - { - tryFormattingCode(editorRef, setCodeString); - validateCode( - codeString, - codeFile, - dispatchOutput, - stepIndex, - chapterIndex, - ); - }} - variant={ - outputResult.validityStatus === "valid" ? "success" : "default" - } - tooltip="Shift + Enter" - > - Validate - - - { - setCodeString(JSON.stringify(codeFile.code, null, 2)); - dispatchOutput({ type: "RESET" }); - }} - variant={"error"} - > - Reset - - - { - if (nextStepPath) router.push("/" + nextStepPath); - }} - variant={ - outputResult.validityStatus === "valid" ? "default" : "success" - } - isDisabled={!nextStepPath} - size={outputResult.validityStatus === "valid" ? "sm" : "xs"} - > - Next - - -
+ ); }