3
3
import styles from "./CodeEditor.module.css" ;
4
4
import ctx from "classnames" ;
5
5
import { GeistMono } from "geist/font/mono" ;
6
- import Editor from "@monaco-editor/react" ;
6
+ import Editor , { Monaco } from "@monaco-editor/react" ;
7
7
import { Flex , useColorMode } from "@chakra-ui/react" ;
8
8
import { useEffect , useState , useRef } from "react" ;
9
9
import MyBtn from "../MyBtn" ;
@@ -14,34 +14,10 @@ import { useUserSolutionStore, useEditorStore } from "@/lib/stores";
14
14
import { sendGAEvent } from "@next/third-parties/google" ;
15
15
import { CodeFile , OutputResult } from "@/lib/types" ;
16
16
import { OutputReducerAction } from "@/lib/reducers" ;
17
+ import CertificateButton from "../CertificateButton/CertificateButton" ;
17
18
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" ) => {
45
21
useEffect ( ( ) => {
46
22
if ( monaco ) {
47
23
monaco . editor . defineTheme ( "my-theme" , {
@@ -55,10 +31,16 @@ export default function CodeEditor({
55
31
monaco . editor . setTheme ( colorMode === "light" ? "light" : "my-theme" ) ;
56
32
}
57
33
} , [ monaco , colorMode ] ) ;
34
+ } ;
58
35
36
+ // Custom hook for keyboard shortcuts
37
+ const useValidationShortcut = (
38
+ handleValidate : ( ) => void ,
39
+ codeString : string ,
40
+ ) => {
59
41
useEffect ( ( ) => {
60
- const handleKeyDown = ( event ) => {
61
- if ( event . key == "Enter" && event . shiftKey ) {
42
+ const handleKeyDown = ( event : KeyboardEvent ) => {
43
+ if ( event . key === "Enter" && event . shiftKey ) {
62
44
sendGAEvent ( "event" , "buttonClicked" , {
63
45
value : "Validate (through shortcut)" ,
64
46
} ) ;
@@ -71,8 +53,20 @@ export default function CodeEditor({
71
53
return ( ) => {
72
54
document . removeEventListener ( "keydown" , handleKeyDown ) ;
73
55
} ;
74
- } , [ codeString ] ) ;
56
+ } , [ handleValidate , codeString ] ) ;
57
+ } ;
75
58
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
76
70
useEffect ( ( ) => {
77
71
const savedCode = userSolutionStore . getSavedUserSolutionByLesson (
78
72
chapterIndex ,
@@ -83,6 +77,7 @@ export default function CodeEditor({
83
77
}
84
78
} , [ chapterIndex , stepIndex ] ) ;
85
79
80
+ // Save code changes
86
81
useEffect ( ( ) => {
87
82
userSolutionStore . saveUserSolutionForLesson (
88
83
chapterIndex ,
@@ -91,11 +86,104 @@ export default function CodeEditor({
91
86
) ;
92
87
} , [ codeString , chapterIndex , stepIndex ] ) ;
93
88
89
+ // Initialize code if no saved solutions
94
90
useEffect ( ( ) => {
95
- if ( Object . keys ( userSolutionStore . userSolutionsByLesson ) . length == 0 ) {
91
+ if ( Object . keys ( userSolutionStore . userSolutionsByLesson ) . length === 0 ) {
96
92
setCodeString ( JSON . stringify ( codeFile . code , null , 2 ) ) ;
97
93
}
98
94
} , [ 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 ) ;
99
187
100
188
const handleValidate = ( ) => {
101
189
setIsValidating ( true ) ;
@@ -112,6 +200,28 @@ export default function CodeEditor({
112
200
} , 500 ) ;
113
201
} ;
114
202
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
+
115
225
return (
116
226
< >
117
227
< div className = { ctx ( styles . codeEditor , GeistMono . className ) } >
@@ -128,57 +238,16 @@ export default function CodeEditor({
128
238
formatOnPaste : true ,
129
239
formatOnType : true ,
130
240
} }
131
- onMount = { ( editor , monaco ) => {
132
- setMonaco ( monaco ) ;
133
- editorRef . current = editor ;
134
- editorStore . setEditor ( editor ) ;
135
- editorStore . setMonaco ( monaco ) ;
136
- } }
241
+ onMount = { handleEditorMount }
137
242
/>
138
243
</ 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
+ />
182
251
</ >
183
252
) ;
184
253
}
0 commit comments