33import styles from "./CodeEditor.module.css" ;
44import ctx from "classnames" ;
55import { GeistMono } from "geist/font/mono" ;
6- import Editor from "@monaco-editor/react" ;
6+ import Editor , { Monaco } from "@monaco-editor/react" ;
77import { Flex , useColorMode } from "@chakra-ui/react" ;
88import { useEffect , useState , useRef } from "react" ;
99import MyBtn from "../MyBtn" ;
@@ -14,34 +14,10 @@ import { useUserSolutionStore, useEditorStore } from "@/lib/stores";
1414import { sendGAEvent } from "@next/third-parties/google" ;
1515import { CodeFile , OutputResult } from "@/lib/types" ;
1616import { OutputReducerAction } from "@/lib/reducers" ;
17+ import CertificateButton from "../CertificateButton/CertificateButton" ;
1718
18- export default function CodeEditor ( {
19- codeString,
20- setCodeString,
21- codeFile,
22- dispatchOutput,
23- nextStepPath,
24- stepIndex,
25- chapterIndex,
26- outputResult,
27- } : {
28- codeString : string ;
29- setCodeString : ( codeString : string ) => void ;
30- codeFile : CodeFile ;
31- dispatchOutput : React . Dispatch < OutputReducerAction > ;
32- nextStepPath : string | undefined ;
33- stepIndex : number ;
34- chapterIndex : number ;
35- outputResult : OutputResult ;
36- } ) {
37- const { colorMode } = useColorMode ( ) ;
38- const [ monaco , setMonaco ] = useState < any > ( null ) ;
39- const [ isValidating , setIsValidating ] = useState ( false ) ;
40- const router = useRouter ( ) ;
41- const editorStore = useEditorStore ( ) ;
42- const userSolutionStore = useUserSolutionStore ( ) ;
43- const editorRef = useRef < any > ( null ) ;
44-
19+ // Custom hook for editor theme setup
20+ const useEditorTheme = ( monaco : Monaco , colorMode : "dark" | "light" ) => {
4521 useEffect ( ( ) => {
4622 if ( monaco ) {
4723 monaco . editor . defineTheme ( "my-theme" , {
@@ -55,10 +31,16 @@ export default function CodeEditor({
5531 monaco . editor . setTheme ( colorMode === "light" ? "light" : "my-theme" ) ;
5632 }
5733 } , [ monaco , colorMode ] ) ;
34+ } ;
5835
36+ // Custom hook for keyboard shortcuts
37+ const useValidationShortcut = (
38+ handleValidate : ( ) => void ,
39+ codeString : string ,
40+ ) => {
5941 useEffect ( ( ) => {
60- const handleKeyDown = ( event ) => {
61- if ( event . key == "Enter" && event . shiftKey ) {
42+ const handleKeyDown = ( event : KeyboardEvent ) => {
43+ if ( event . key === "Enter" && event . shiftKey ) {
6244 sendGAEvent ( "event" , "buttonClicked" , {
6345 value : "Validate (through shortcut)" ,
6446 } ) ;
@@ -71,8 +53,20 @@ export default function CodeEditor({
7153 return ( ) => {
7254 document . removeEventListener ( "keydown" , handleKeyDown ) ;
7355 } ;
74- } , [ codeString ] ) ;
56+ } , [ handleValidate , codeString ] ) ;
57+ } ;
7558
59+ // Custom hook for code persistence
60+ const useCodePersistence = (
61+ chapterIndex : number ,
62+ stepIndex : number ,
63+ codeString : string ,
64+ setCodeString : ( value : string ) => void ,
65+ codeFile : CodeFile ,
66+ ) => {
67+ const userSolutionStore = useUserSolutionStore ( ) ;
68+
69+ // Load saved code
7670 useEffect ( ( ) => {
7771 const savedCode = userSolutionStore . getSavedUserSolutionByLesson (
7872 chapterIndex ,
@@ -83,6 +77,7 @@ export default function CodeEditor({
8377 }
8478 } , [ chapterIndex , stepIndex ] ) ;
8579
80+ // Save code changes
8681 useEffect ( ( ) => {
8782 userSolutionStore . saveUserSolutionForLesson (
8883 chapterIndex ,
@@ -91,11 +86,104 @@ export default function CodeEditor({
9186 ) ;
9287 } , [ codeString , chapterIndex , stepIndex ] ) ;
9388
89+ // Initialize code if no saved solutions
9490 useEffect ( ( ) => {
95- if ( Object . keys ( userSolutionStore . userSolutionsByLesson ) . length == 0 ) {
91+ if ( Object . keys ( userSolutionStore . userSolutionsByLesson ) . length === 0 ) {
9692 setCodeString ( JSON . stringify ( codeFile . code , null , 2 ) ) ;
9793 }
9894 } , [ userSolutionStore ] ) ;
95+ } ;
96+
97+ // EditorControls component for the buttons section
98+ const EditorControls = ( {
99+ handleValidate,
100+ isValidating,
101+ resetCode,
102+ nextStepPath,
103+ outputResult,
104+ } : {
105+ handleValidate : ( ) => void ;
106+ isValidating : boolean ;
107+ resetCode : ( ) => void ;
108+ nextStepPath : string | undefined ;
109+ outputResult : OutputResult ;
110+ } ) => {
111+ const router = useRouter ( ) ;
112+
113+ return (
114+ < div className = { styles . buttonsWrapper } >
115+ < Flex dir = "row" gap = "8px" alignItems = "end" >
116+ < MyBtn
117+ onClick = { handleValidate }
118+ variant = {
119+ outputResult . validityStatus === "valid" ? "success" : "default"
120+ }
121+ isDisabled = { isValidating }
122+ tooltip = "Shift + Enter"
123+ >
124+ { isValidating ? "Validating ..." : "Validate" }
125+ </ MyBtn >
126+
127+ < MyBtn onClick = { resetCode } variant = "error" >
128+ Reset
129+ </ MyBtn >
130+ </ Flex >
131+ { nextStepPath ? (
132+ < >
133+ < MyBtn
134+ onClick = { ( ) => {
135+ if ( nextStepPath ) router . push ( "/" + nextStepPath ) ;
136+ } }
137+ variant = {
138+ outputResult . validityStatus === "valid" ? "default" : "success"
139+ }
140+ isDisabled = { ! ! isValidating }
141+ size = { outputResult . validityStatus === "valid" ? "sm" : "xs" }
142+ >
143+ Next < span style = { { marginLeft : "4px" } } > </ span >
144+ < FiChevronRight
145+ color = {
146+ outputResult . validityStatus === "valid"
147+ ? "white"
148+ : "hsl(var(--success))"
149+ }
150+ />
151+ </ MyBtn >
152+ </ >
153+ ) : (
154+ < CertificateButton />
155+ ) }
156+ </ div >
157+ ) ;
158+ } ;
159+
160+ export default function CodeEditor ( {
161+ codeString,
162+ setCodeString,
163+ codeFile,
164+ dispatchOutput,
165+ nextStepPath,
166+ stepIndex,
167+ chapterIndex,
168+ outputResult,
169+ } : {
170+ codeString : string ;
171+ setCodeString : ( codeString : string ) => void ;
172+ codeFile : CodeFile ;
173+ dispatchOutput : React . Dispatch < OutputReducerAction > ;
174+ nextStepPath : string | undefined ;
175+ stepIndex : number ;
176+ chapterIndex : number ;
177+ outputResult : OutputResult ;
178+ } ) {
179+ const { colorMode } = useColorMode ( ) ;
180+ const [ monaco , setMonaco ] = useState < any > ( null ) ;
181+ const [ isValidating , setIsValidating ] = useState ( false ) ;
182+ const editorStore = useEditorStore ( ) ;
183+ const editorRef = useRef < any > ( null ) ;
184+
185+ // Apply custom hooks
186+ useEditorTheme ( monaco , colorMode ) ;
99187
100188 const handleValidate = ( ) => {
101189 setIsValidating ( true ) ;
@@ -112,6 +200,28 @@ export default function CodeEditor({
112200 } , 500 ) ;
113201 } ;
114202
203+ useValidationShortcut ( handleValidate , codeString ) ;
204+ useCodePersistence (
205+ chapterIndex ,
206+ stepIndex ,
207+ codeString ,
208+ setCodeString ,
209+ codeFile ,
210+ ) ;
211+
212+ const resetCode = ( ) => {
213+ setCodeString ( JSON . stringify ( codeFile . code , null , 2 ) ) ;
214+ dispatchOutput ( { type : "RESET" } ) ;
215+ } ;
216+
217+ const handleEditorMount = ( editor : any , monaco : Monaco ) => {
218+ setMonaco ( monaco ) ;
219+
220+ editorRef . current = editor ;
221+ editorStore . setEditor ( editor ) ;
222+ editorStore . setMonaco ( monaco ) ;
223+ } ;
224+
115225 return (
116226 < >
117227 < div className = { ctx ( styles . codeEditor , GeistMono . className ) } >
@@ -128,57 +238,16 @@ export default function CodeEditor({
128238 formatOnPaste : true ,
129239 formatOnType : true ,
130240 } }
131- onMount = { ( editor , monaco ) => {
132- setMonaco ( monaco ) ;
133- editorRef . current = editor ;
134- editorStore . setEditor ( editor ) ;
135- editorStore . setMonaco ( monaco ) ;
136- } }
241+ onMount = { handleEditorMount }
137242 />
138243 </ div >
139- < div className = { styles . buttonsWrapper } >
140- < Flex dir = "row" gap = "8px" alignItems = "end" >
141- < MyBtn
142- onClick = { ( ) => handleValidate ( ) }
143- variant = {
144- outputResult . validityStatus === "valid" ? "success" : "default"
145- }
146- isDisabled = { isValidating }
147- tooltip = "Shift + Enter"
148- >
149- { isValidating ? "Validating..." : "Validate" }
150- </ MyBtn >
151-
152- < MyBtn
153- onClick = { ( ) => {
154- setCodeString ( JSON . stringify ( codeFile . code , null , 2 ) ) ;
155- dispatchOutput ( { type : "RESET" } ) ;
156- } }
157- variant = "error"
158- >
159- Reset
160- </ MyBtn >
161- </ Flex >
162- < MyBtn
163- onClick = { ( ) => {
164- if ( nextStepPath ) router . push ( "/" + nextStepPath ) ;
165- } }
166- variant = {
167- outputResult . validityStatus === "valid" ? "default" : "success"
168- }
169- isDisabled = { ! nextStepPath }
170- size = { outputResult . validityStatus === "valid" ? "sm" : "xs" }
171- >
172- Next < span style = { { marginLeft : "4px" } } > </ span >
173- < FiChevronRight
174- color = {
175- outputResult . validityStatus === "valid"
176- ? "white"
177- : "hsl(var(--success))"
178- }
179- />
180- </ MyBtn >
181- </ div >
244+ < EditorControls
245+ handleValidate = { handleValidate }
246+ isValidating = { isValidating }
247+ resetCode = { resetCode }
248+ nextStepPath = { nextStepPath }
249+ outputResult = { outputResult }
250+ />
182251 </ >
183252 ) ;
184253}
0 commit comments