diff --git a/app/components/CodeEditor/CodeEditor.tsx b/app/components/CodeEditor/CodeEditor.tsx index 32870c3..a0d20e0 100644 --- a/app/components/CodeEditor/CodeEditor.tsx +++ b/app/components/CodeEditor/CodeEditor.tsx @@ -5,9 +5,14 @@ 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 { + tryFormattingCode, + validateCode, + restorePreviousValidation, + hasValidationResult, +} from "@/lib/client-functions"; import FiChevronRight from "@/app/styles/icons/HiChevronRightGreen"; import { useRouter } from "next/navigation"; import { useUserSolutionStore, useEditorStore } from "@/lib/stores"; @@ -94,6 +99,39 @@ const useCodePersistence = ( }, [userSolutionStore]); }; +// Custom hook for validation restoration +const useValidationRestore = ( + chapterIndex: number, + stepIndex: number, + dispatchOutput: React.Dispatch, + setCodeString: (value: string) => void, +) => { + const [isRestored, setIsRestored] = useState(false); + + useEffect(() => { + // Restore previous validation on component mount or when lesson changes + 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 }; +}; + + // EditorControls component for the buttons section const EditorControls = ({ handleValidate, @@ -179,7 +217,7 @@ export default function CodeEditor({ // Apply custom hooks useEditorTheme(monaco, colorMode); - const handleValidate = () => { + const handleValidate = useCallback(() => { setIsValidating(true); setTimeout(() => { tryFormattingCode(editorRef, setCodeString); @@ -192,7 +230,7 @@ export default function CodeEditor({ ); setIsValidating(false); }, 500); - }; + }, [codeString, codeFile, dispatchOutput, stepIndex, chapterIndex]); useValidationShortcut(handleValidate, codeString); useCodePersistence( @@ -203,21 +241,43 @@ export default function CodeEditor({ codeFile, ); + const { isRestored } = useValidationRestore( + chapterIndex, + stepIndex, + dispatchOutput, + setCodeString, + ); + const resetCode = () => { setCodeString(JSON.stringify(codeFile.code, null, 2)); dispatchOutput({ type: "RESET" }); }; - const handleEditorMount = (editor: any, monaco: Monaco) => { - setMonaco(monaco); + const handleEditorMount = (editor: monaco.editor.IStandaloneCodeEditor, monacoInstance: Monaco) => { + setMonaco(monacoInstance); editorRef.current = editor; editorStore.setEditor(editor); - editorStore.setMonaco(monaco); + editorStore.setMonaco(monacoInstance); }; return ( <> + {isRestored && ( +
+ ✅ Previous submission restored +
+ )} +
{ + if (a.passed === b.passed) { + return 0; + } + return a.passed ? 1 : -1; + }); + + // Save validation result to localStorage BEFORE dispatching + saveValidationResult( + chapterIndex, + stepIndex, + codeString, + sortedResults, + totalTestCases, + validationStatus, + ); + if (validationStatus === "valid") { - const sortedResults = testCaseResults.sort((a, b) => { - if (a.passed === b.passed) { - return 0; // If both are the same, their order doesn't change - } - return a.passed ? 1 : -1; // If a.passed is true, put a after b; if false, put a before b - }); dispatchOutput({ type: "valid", payload: { testCaseResults: sortedResults, totalTestCases }, @@ -72,13 +89,7 @@ export async function validateCode( sendGAEvent("event", "validation", { validation_result: "passed", }); - } else { - const sortedResults = testCaseResults.sort((a, b) => { - if (a.passed === b.passed) { - return 0; // If both are the same, their order doesn't change - } - return a.passed ? 1 : -1; // If a.passed is true, put a after b; if false, put a before b - }); + } else { dispatchOutput({ type: "invalid", payload: { testCaseResults: sortedResults, totalTestCases }, @@ -88,6 +99,9 @@ export async function validateCode( }); } } catch (e) { + // Save error state as well + saveValidationResult(chapterIndex, stepIndex, codeString, [], 0, "invalid"); + if ((e as Error).message === "Invalid Schema") { dispatchOutput({ type: "invalidSchema", @@ -193,3 +207,45 @@ export async function tryFormattingCode( return; } } + +export function restorePreviousValidation( + chapterIndex: number, + stepIndex: number, + dispatchOutput: React.Dispatch, + setCodeString?: (code: string) => void +): { restored: boolean; code?: string } { + if (typeof window === "undefined") return { restored: false }; + + const validationResult = getValidationResult(chapterIndex, stepIndex); + + if (validationResult) { + // Restore code if setter provided + if (setCodeString) { + setCodeString(validationResult.code); + } + + // Restore validation results + if (validationResult.validationStatus === "valid") { + dispatchOutput({ + type: "valid", + payload: { + testCaseResults: validationResult.testCaseResults, + totalTestCases: validationResult.totalTestCases + }, + }); + } else if (validationResult.validationStatus === "invalid") { + dispatchOutput({ + type: "invalid", + payload: { + testCaseResults: validationResult.testCaseResults, + totalTestCases: validationResult.totalTestCases + }, + }); + } + + return { restored: true, code: validationResult.code }; + } + + return { restored: false }; +} +export { hasValidationResult } from "./progressSaving"; \ No newline at end of file diff --git a/lib/progressSaving.ts b/lib/progressSaving.ts index 6f139a7..c856784 100644 --- a/lib/progressSaving.ts +++ b/lib/progressSaving.ts @@ -1,3 +1,75 @@ +interface ValidationResult { + code: string; + testCaseResults: any[]; + totalTestCases: number; + validationStatus: "valid" | "invalid" | "neutral"; + timestamp: number; + chapterIndex: number; + stepIndex: number; +} + +export function saveValidationResult( + chapterIndex: number, + stepIndex: number, + code: string, + testCaseResults: any[], + totalTestCases: number, + validationStatus: "valid" | "invalid" | "neutral" +): boolean { + if (typeof window === "undefined") return false; + + const key = `validation-${chapterIndex}-${stepIndex}`; + const validationData: ValidationResult = { + code, + testCaseResults, + totalTestCases, + validationStatus, + timestamp: Date.now(), + chapterIndex, + stepIndex + }; + + try { + localStorage.setItem(key, JSON.stringify(validationData)); + return true; + } catch (error) { + console.warn('Failed to save validation result:', error); + return false; + } +} + +export function getValidationResult(chapterIndex: number, stepIndex: number): ValidationResult | null { + if (typeof window === "undefined") return null; + + const key = `validation-${chapterIndex}-${stepIndex}`; + const stored = localStorage.getItem(key); + + if (stored) { + try { + return JSON.parse(stored); + } catch (error) { + console.warn('Failed to parse validation result:', error); + return null; + } + } + return null; +} + +export function hasValidationResult(chapterIndex: number, stepIndex: number): boolean { + if (typeof window === "undefined") return false; + + const key = `validation-${chapterIndex}-${stepIndex}`; + return localStorage.getItem(key) !== null; +} + +export function clearValidationResult(chapterIndex: number, stepIndex: number): boolean { + if (typeof window === "undefined") return false; + + const key = `validation-${chapterIndex}-${stepIndex}`; + localStorage.removeItem(key); + return true; +} + export function setCheckpoint(path: string) { if (typeof window === "undefined") return false; const checkpoint = path;