Skip to content

Commit 54f4305

Browse files
committed
refactor: modularize hooks and improve validation persistence logic
1 parent d5c2162 commit 54f4305

File tree

4 files changed

+179
-124
lines changed

4 files changed

+179
-124
lines changed

app/components/CodeEditor/CodeEditor.tsx

Lines changed: 13 additions & 120 deletions
Original file line numberDiff line numberDiff line change
@@ -7,130 +7,20 @@ import Editor, { Monaco } from "@monaco-editor/react";
77
import { Flex, useColorMode } from "@chakra-ui/react";
88
import { useEffect, useState, useRef, useCallback } from "react";
99
import MyBtn from "../MyBtn";
10-
import {
11-
tryFormattingCode,
12-
validateCode,
13-
restorePreviousValidation,
14-
hasValidationResult,
15-
} from "@/lib/client-functions";
10+
import { tryFormattingCode, validateCode } from "@/lib/client-functions";
1611
import FiChevronRight from "@/app/styles/icons/HiChevronRightGreen";
1712
import { useRouter } from "next/navigation";
18-
import { useUserSolutionStore, useEditorStore } from "@/lib/stores";
13+
import { useEditorStore } from "@/lib/stores";
1914
import { sendGAEvent } from "@next/third-parties/google";
2015
import { CodeFile, OutputResult } from "@/lib/types";
2116
import { OutputReducerAction } from "@/lib/reducers";
2217
import CertificateButton from "../CertificateButton/CertificateButton";
23-
24-
// Custom hook for editor theme setup
25-
const useEditorTheme = (monaco: Monaco, colorMode: "dark" | "light") => {
26-
useEffect(() => {
27-
if (monaco) {
28-
monaco.editor.defineTheme("my-theme", {
29-
base: "vs-dark",
30-
inherit: true,
31-
rules: [],
32-
colors: {
33-
"editor.background": "#1f1f1f",
34-
},
35-
});
36-
monaco.editor.setTheme(colorMode === "light" ? "light" : "my-theme");
37-
}
38-
}, [monaco, colorMode]);
39-
};
40-
41-
// Custom hook for keyboard shortcuts
42-
const useValidationShortcut = (
43-
handleValidate: () => void,
44-
codeString: string,
45-
) => {
46-
useEffect(() => {
47-
const handleKeyDown = (event: KeyboardEvent) => {
48-
if (event.key === "Enter" && event.shiftKey) {
49-
sendGAEvent("event", "buttonClicked", {
50-
value: "Validate (through shortcut)",
51-
});
52-
event.preventDefault();
53-
handleValidate();
54-
}
55-
};
56-
57-
document.addEventListener("keydown", handleKeyDown);
58-
return () => {
59-
document.removeEventListener("keydown", handleKeyDown);
60-
};
61-
}, [handleValidate, codeString]);
62-
};
63-
64-
// Custom hook for code persistence
65-
const useCodePersistence = (
66-
chapterIndex: number,
67-
stepIndex: number,
68-
codeString: string,
69-
setCodeString: (value: string) => void,
70-
codeFile: CodeFile,
71-
) => {
72-
const userSolutionStore = useUserSolutionStore();
73-
74-
// Load saved code
75-
useEffect(() => {
76-
const savedCode = userSolutionStore.getSavedUserSolutionByLesson(
77-
chapterIndex,
78-
stepIndex,
79-
);
80-
if (savedCode && savedCode !== codeString) {
81-
setCodeString(savedCode);
82-
}
83-
}, [chapterIndex, stepIndex]);
84-
85-
// Save code changes
86-
useEffect(() => {
87-
userSolutionStore.saveUserSolutionForLesson(
88-
chapterIndex,
89-
stepIndex,
90-
codeString,
91-
);
92-
}, [codeString, chapterIndex, stepIndex]);
93-
94-
// Initialize code if no saved solutions
95-
useEffect(() => {
96-
if (Object.keys(userSolutionStore.userSolutionsByLesson).length === 0) {
97-
setCodeString(JSON.stringify(codeFile.code, null, 2));
98-
}
99-
}, [userSolutionStore]);
100-
};
101-
102-
// Custom hook for validation restoration
103-
const useValidationRestore = (
104-
chapterIndex: number,
105-
stepIndex: number,
106-
dispatchOutput: React.Dispatch<OutputReducerAction>,
107-
setCodeString: (value: string) => void,
108-
) => {
109-
const [isRestored, setIsRestored] = useState(false);
110-
111-
useEffect(() => {
112-
// Restore previous validation on component mount or when lesson changes
113-
if (!isRestored && hasValidationResult(chapterIndex, stepIndex)) {
114-
try {
115-
const { restored } = restorePreviousValidation(
116-
chapterIndex,
117-
stepIndex,
118-
dispatchOutput,
119-
setCodeString
120-
);
121-
if (restored) {
122-
setIsRestored(true);
123-
console.log('✅ Previous validation restored for lesson:', chapterIndex, stepIndex);
124-
}
125-
} catch (error) {
126-
console.error('Failed to restore validation:', error);
127-
}
128-
}
129-
}, [chapterIndex, stepIndex, isRestored, dispatchOutput, setCodeString]);
130-
131-
return { isRestored };
132-
};
133-
18+
import {
19+
useEditorTheme,
20+
useValidationShortcut,
21+
useCodePersistence,
22+
useValidationRestore,
23+
} from "@/app/utils/hooks";
13424

13525
// EditorControls component for the buttons section
13626
const EditorControls = ({
@@ -230,7 +120,7 @@ export default function CodeEditor({
230120
);
231121
setIsValidating(false);
232122
}, 500);
233-
}, [codeString, codeFile, dispatchOutput, stepIndex, chapterIndex]);
123+
}, [codeString, codeFile, dispatchOutput, stepIndex, chapterIndex, setCodeString]);
234124

235125
useValidationShortcut(handleValidate, codeString);
236126
useCodePersistence(
@@ -241,19 +131,21 @@ export default function CodeEditor({
241131
codeFile,
242132
);
243133

134+
// Restore previous validation on lesson revisit
244135
const { isRestored } = useValidationRestore(
245136
chapterIndex,
246137
stepIndex,
247138
dispatchOutput,
248139
setCodeString,
249140
);
250141

142+
// Reset code to initial state
251143
const resetCode = () => {
252144
setCodeString(JSON.stringify(codeFile.code, null, 2));
253145
dispatchOutput({ type: "RESET" });
254146
};
255147

256-
const handleEditorMount = (editor: monaco.editor.IStandaloneCodeEditor, monacoInstance: Monaco) => {
148+
const handleEditorMount = (editor: any, monacoInstance: Monaco) => {
257149
setMonaco(monacoInstance);
258150

259151
editorRef.current = editor;
@@ -263,6 +155,7 @@ export default function CodeEditor({
263155

264156
return (
265157
<>
158+
{/* Show success banner when previous validation is restored */}
266159
{isRestored && (
267160
<div
268161
style={{

app/utils/hooks.ts

Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
import { useEffect, useState } from "react";
2+
import { Monaco } from "@monaco-editor/react";
3+
import { sendGAEvent } from "@next/third-parties/google";
4+
import { useUserSolutionStore } from "@/lib/stores";
5+
import { CodeFile } from "@/lib/types";
6+
import { OutputReducerAction } from "@/lib/reducers";
7+
import {
8+
restorePreviousValidation,
9+
hasValidationResult,
10+
} from "@/lib/client-functions";
11+
12+
/**
13+
* Hook to configure Monaco editor theme based on color mode
14+
* Applies custom dark theme or light theme to the editor
15+
*/
16+
export const useEditorTheme = (
17+
monaco: Monaco | null,
18+
colorMode: "dark" | "light",
19+
) => {
20+
useEffect(() => {
21+
if (monaco) {
22+
monaco.editor.defineTheme("my-theme", {
23+
base: "vs-dark",
24+
inherit: true,
25+
rules: [],
26+
colors: {
27+
"editor.background": "#1f1f1f",
28+
},
29+
});
30+
monaco.editor.setTheme(colorMode === "light" ? "light" : "my-theme");
31+
}
32+
}, [monaco, colorMode]);
33+
};
34+
35+
/**
36+
* Hook to handle keyboard shortcuts for validation
37+
* Triggers validation when Shift+Enter is pressed
38+
*/
39+
export const useValidationShortcut = (
40+
handleValidate: () => void,
41+
codeString: string,
42+
) => {
43+
useEffect(() => {
44+
const handleKeyDown = (event: KeyboardEvent) => {
45+
if (event.key === "Enter" && event.shiftKey) {
46+
sendGAEvent("event", "buttonClicked", {
47+
value: "Validate (through shortcut)",
48+
});
49+
event.preventDefault();
50+
handleValidate();
51+
}
52+
};
53+
54+
document.addEventListener("keydown", handleKeyDown);
55+
return () => {
56+
document.removeEventListener("keydown", handleKeyDown);
57+
};
58+
}, [handleValidate, codeString]);
59+
};
60+
61+
/**
62+
* Hook to persist user code in localStorage across sessions
63+
* Loads saved code on mount and saves changes automatically
64+
*/
65+
export const useCodePersistence = (
66+
chapterIndex: number,
67+
stepIndex: number,
68+
codeString: string,
69+
setCodeString: (value: string) => void,
70+
codeFile: CodeFile,
71+
) => {
72+
const userSolutionStore = useUserSolutionStore();
73+
74+
// Load saved code on mount or lesson change
75+
useEffect(() => {
76+
const savedCode = userSolutionStore.getSavedUserSolutionByLesson(
77+
chapterIndex,
78+
stepIndex,
79+
);
80+
if (savedCode && savedCode !== codeString) {
81+
setCodeString(savedCode);
82+
}
83+
// eslint-disable-next-line react-hooks/exhaustive-deps
84+
}, [chapterIndex, stepIndex]);
85+
86+
// Save code changes to localStorage
87+
useEffect(() => {
88+
userSolutionStore.saveUserSolutionForLesson(
89+
chapterIndex,
90+
stepIndex,
91+
codeString,
92+
);
93+
// eslint-disable-next-line react-hooks/exhaustive-deps
94+
}, [codeString, chapterIndex, stepIndex]);
95+
96+
// Initialize with default code if no saved solutions exist
97+
useEffect(() => {
98+
if (Object.keys(userSolutionStore.userSolutionsByLesson).length === 0) {
99+
setCodeString(JSON.stringify(codeFile.code, null, 2));
100+
}
101+
// eslint-disable-next-line react-hooks/exhaustive-deps
102+
}, [userSolutionStore]);
103+
};
104+
105+
/**
106+
* Hook to restore previous validation results when revisiting a lesson
107+
* Automatically loads and displays saved validation state from localStorage
108+
* Returns isRestored flag to show restoration status to user
109+
*/
110+
export const useValidationRestore = (
111+
chapterIndex: number,
112+
stepIndex: number,
113+
dispatchOutput: React.Dispatch<OutputReducerAction>,
114+
setCodeString: (value: string) => void,
115+
) => {
116+
const [isRestored, setIsRestored] = useState(false);
117+
118+
useEffect(() => {
119+
// Check if previous validation exists before restoring
120+
if (!isRestored && hasValidationResult(chapterIndex, stepIndex)) {
121+
try {
122+
const { restored } = restorePreviousValidation(
123+
chapterIndex,
124+
stepIndex,
125+
dispatchOutput,
126+
setCodeString,
127+
);
128+
if (restored) {
129+
setIsRestored(true);
130+
console.log(
131+
"✅ Previous validation restored for lesson:",
132+
chapterIndex,
133+
stepIndex,
134+
);
135+
}
136+
} catch (error) {
137+
console.error("Failed to restore validation:", error);
138+
}
139+
}
140+
}, [chapterIndex, stepIndex, isRestored, dispatchOutput, setCodeString]);
141+
142+
return { isRestored };
143+
};

lib/client-functions.ts

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -63,14 +63,15 @@ export async function validateCode(
6363
await hyperjumpCheckAnnotations(schemaCode, codeFile.expectedAnnotations);
6464
}
6565

66+
// Sort results: failed tests first, passed tests last
6667
const sortedResults = testCaseResults.sort((a, b) => {
6768
if (a.passed === b.passed) {
6869
return 0;
6970
}
7071
return a.passed ? 1 : -1;
7172
});
7273

73-
// Save validation result to localStorage BEFORE dispatching
74+
// Persist validation results to localStorage for restoration on revisit
7475
saveValidationResult(
7576
chapterIndex,
7677
stepIndex,
@@ -99,7 +100,7 @@ export async function validateCode(
99100
});
100101
}
101102
} catch (e) {
102-
// Save error state as well
103+
// Persist error state for restoration on revisit
103104
saveValidationResult(chapterIndex, stepIndex, codeString, [], 0, "invalid");
104105

105106
if ((e as Error).message === "Invalid Schema") {
@@ -208,6 +209,10 @@ export async function tryFormattingCode(
208209
}
209210
}
210211

212+
/**
213+
* Restore previous validation results when revisiting a lesson
214+
* Automatically restores both code and validation state from localStorage
215+
*/
211216
export function restorePreviousValidation(
212217
chapterIndex: number,
213218
stepIndex: number,
@@ -219,12 +224,12 @@ export function restorePreviousValidation(
219224
const validationResult = getValidationResult(chapterIndex, stepIndex);
220225

221226
if (validationResult) {
222-
// Restore code if setter provided
227+
// Restore user's submitted code
223228
if (setCodeString) {
224229
setCodeString(validationResult.code);
225230
}
226231

227-
// Restore validation results
232+
// Restore validation output state
228233
if (validationResult.validationStatus === "valid") {
229234
dispatchOutput({
230235
type: "valid",

0 commit comments

Comments
 (0)