Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
123 changes: 38 additions & 85 deletions app/components/CodeEditor/CodeEditor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,94 +5,22 @@ import ctx from "classnames";
import { GeistMono } from "geist/font/mono";
import Editor, { Monaco } from "@monaco-editor/react";
import { Flex, useColorMode } from "@chakra-ui/react";
import { useEffect, useState, useRef } from "react";
import { useEffect, useState, useRef, useCallback } from "react";
import MyBtn from "../MyBtn";
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 { useEditorStore } from "@/lib/stores";
import { sendGAEvent } from "@next/third-parties/google";
import { CodeFile, OutputResult } from "@/lib/types";
import { OutputReducerAction } from "@/lib/reducers";
import CertificateButton from "../CertificateButton/CertificateButton";

// Custom hook for editor theme setup
const useEditorTheme = (monaco: Monaco, colorMode: "dark" | "light") => {
useEffect(() => {
if (monaco) {
monaco.editor.defineTheme("my-theme", {
base: "vs-dark",
inherit: true,
rules: [],
colors: {
"editor.background": "#1f1f1f",
},
});
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) {
sendGAEvent("event", "buttonClicked", {
value: "Validate (through shortcut)",
});
event.preventDefault();
handleValidate();
}
};

document.addEventListener("keydown", handleKeyDown);
return () => {
document.removeEventListener("keydown", handleKeyDown);
};
}, [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,
stepIndex,
);
if (savedCode && savedCode !== codeString) {
setCodeString(savedCode);
}
}, [chapterIndex, stepIndex]);

// Save code changes
useEffect(() => {
userSolutionStore.saveUserSolutionForLesson(
chapterIndex,
stepIndex,
codeString,
);
}, [codeString, chapterIndex, stepIndex]);

// Initialize code if no saved solutions
useEffect(() => {
if (Object.keys(userSolutionStore.userSolutionsByLesson).length === 0) {
setCodeString(JSON.stringify(codeFile.code, null, 2));
}
}, [userSolutionStore]);
};
import {
useEditorTheme,
useValidationShortcut,
useCodePersistence,
useValidationRestore,
} from "@/app/utils/hooks";

// EditorControls component for the buttons section
const EditorControls = ({
Expand Down Expand Up @@ -179,7 +107,7 @@ export default function CodeEditor({
// Apply custom hooks
useEditorTheme(monaco, colorMode);

const handleValidate = () => {
const handleValidate = useCallback(() => {
setIsValidating(true);
setTimeout(() => {
tryFormattingCode(editorRef, setCodeString);
Expand All @@ -192,7 +120,7 @@ export default function CodeEditor({
);
setIsValidating(false);
}, 500);
};
}, [codeString, codeFile, dispatchOutput, stepIndex, chapterIndex, setCodeString]);

useValidationShortcut(handleValidate, codeString);
useCodePersistence(
Expand All @@ -203,21 +131,46 @@ export default function CodeEditor({
codeFile,
);

// Restore previous validation on lesson revisit
const { isRestored } = useValidationRestore(
chapterIndex,
stepIndex,
dispatchOutput,
setCodeString,
);

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

const handleEditorMount = (editor: any, monaco: Monaco) => {
setMonaco(monaco);
const handleEditorMount = (editor: any, monacoInstance: Monaco) => {
setMonaco(monacoInstance);

editorRef.current = editor;
editorStore.setEditor(editor);
editorStore.setMonaco(monaco);
editorStore.setMonaco(monacoInstance);
};

return (
<>
{/* Show success banner when previous validation is restored */}
{isRestored && (
<div
style={{
padding: "8px 12px",
backgroundColor: "#e8f5e8",
borderLeft: "3px solid #4caf50",
marginBottom: "8px",
fontSize: "14px",
color: "#2e7d32",
}}
>
✅ Previous submission restored
</div>
)}

<div className={ctx(styles.codeEditor, GeistMono.className)}>
<Editor
language="json"
Expand Down
143 changes: 143 additions & 0 deletions app/utils/hooks.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
import { useEffect, useState } from "react";
import { Monaco } from "@monaco-editor/react";
import { sendGAEvent } from "@next/third-parties/google";
import { useUserSolutionStore } from "@/lib/stores";
import { CodeFile } from "@/lib/types";
import { OutputReducerAction } from "@/lib/reducers";
import {
restorePreviousValidation,
hasValidationResult,
} from "@/lib/client-functions";

/**
* Hook to configure Monaco editor theme based on color mode
* Applies custom dark theme or light theme to the editor
*/
export const useEditorTheme = (
monaco: Monaco | null,
colorMode: "dark" | "light",
) => {
useEffect(() => {
if (monaco) {
monaco.editor.defineTheme("my-theme", {
base: "vs-dark",
inherit: true,
rules: [],
colors: {
"editor.background": "#1f1f1f",
},
});
monaco.editor.setTheme(colorMode === "light" ? "light" : "my-theme");
}
}, [monaco, colorMode]);
};

/**
* Hook to handle keyboard shortcuts for validation
* Triggers validation when Shift+Enter is pressed
*/
export const useValidationShortcut = (
handleValidate: () => void,
codeString: string,
) => {
useEffect(() => {
const handleKeyDown = (event: KeyboardEvent) => {
if (event.key === "Enter" && event.shiftKey) {
sendGAEvent("event", "buttonClicked", {
value: "Validate (through shortcut)",
});
event.preventDefault();
handleValidate();
}
};

document.addEventListener("keydown", handleKeyDown);
return () => {
document.removeEventListener("keydown", handleKeyDown);
};
}, [handleValidate, codeString]);
};

/**
* Hook to persist user code in localStorage across sessions
* Loads saved code on mount and saves changes automatically
*/
export const useCodePersistence = (
chapterIndex: number,
stepIndex: number,
codeString: string,
setCodeString: (value: string) => void,
codeFile: CodeFile,
) => {
const userSolutionStore = useUserSolutionStore();

// Load saved code on mount or lesson change
useEffect(() => {
const savedCode = userSolutionStore.getSavedUserSolutionByLesson(
chapterIndex,
stepIndex,
);
if (savedCode && savedCode !== codeString) {
setCodeString(savedCode);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [chapterIndex, stepIndex]);

// Save code changes to localStorage
useEffect(() => {
userSolutionStore.saveUserSolutionForLesson(
chapterIndex,
stepIndex,
codeString,
);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [codeString, chapterIndex, stepIndex]);

// Initialize with default code if no saved solutions exist
useEffect(() => {
if (Object.keys(userSolutionStore.userSolutionsByLesson).length === 0) {
setCodeString(JSON.stringify(codeFile.code, null, 2));
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [userSolutionStore]);
};

/**
* Hook to restore previous validation results when revisiting a lesson
* Automatically loads and displays saved validation state from localStorage
* Returns isRestored flag to show restoration status to user
*/
export const useValidationRestore = (
chapterIndex: number,
stepIndex: number,
dispatchOutput: React.Dispatch<OutputReducerAction>,
setCodeString: (value: string) => void,
) => {
const [isRestored, setIsRestored] = useState(false);

useEffect(() => {
// Check if previous validation exists before restoring
if (!isRestored && hasValidationResult(chapterIndex, stepIndex)) {
try {
const { restored } = restorePreviousValidation(
chapterIndex,
stepIndex,
dispatchOutput,
setCodeString,
);
if (restored) {
setIsRestored(true);
console.log(
"✅ Previous validation restored for lesson:",
chapterIndex,
stepIndex,
);
}
} catch (error) {
console.error("Failed to restore validation:", error);
}
}
}, [chapterIndex, stepIndex, isRestored, dispatchOutput, setCodeString]);

return { isRestored };
};
Loading