From d5c2162259cfd995ad9c8a54c573cd3478a65504 Mon Sep 17 00:00:00 2001 From: Tushar Verma Date: Sun, 14 Sep 2025 15:45:19 +0530 Subject: [PATCH 1/2] added preserve validation function --- app/components/CodeEditor/CodeEditor.tsx | 74 +++++++++++++++++++-- lib/client-functions.ts | 82 ++++++++++++++++++++---- lib/progressSaving.ts | 72 +++++++++++++++++++++ 3 files changed, 208 insertions(+), 20 deletions(-) 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; From 54f430529f13fc959c3d3256096cd17a44fc433e Mon Sep 17 00:00:00 2001 From: Tushar Verma Date: Tue, 4 Nov 2025 13:21:19 +0530 Subject: [PATCH 2/2] refactor: modularize hooks and improve validation persistence logic --- app/components/CodeEditor/CodeEditor.tsx | 133 +++------------------ app/utils/hooks.ts | 143 +++++++++++++++++++++++ lib/client-functions.ts | 13 ++- lib/progressSaving.ts | 14 +++ 4 files changed, 179 insertions(+), 124 deletions(-) create mode 100644 app/utils/hooks.ts diff --git a/app/components/CodeEditor/CodeEditor.tsx b/app/components/CodeEditor/CodeEditor.tsx index a0d20e0..95d5b7b 100644 --- a/app/components/CodeEditor/CodeEditor.tsx +++ b/app/components/CodeEditor/CodeEditor.tsx @@ -7,130 +7,20 @@ import Editor, { Monaco } from "@monaco-editor/react"; import { Flex, useColorMode } from "@chakra-ui/react"; import { useEffect, useState, useRef, useCallback } from "react"; import MyBtn from "../MyBtn"; -import { - tryFormattingCode, - validateCode, - restorePreviousValidation, - hasValidationResult, -} from "@/lib/client-functions"; +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]); -}; - -// 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 }; -}; - +import { + useEditorTheme, + useValidationShortcut, + useCodePersistence, + useValidationRestore, +} from "@/app/utils/hooks"; // EditorControls component for the buttons section const EditorControls = ({ @@ -230,7 +120,7 @@ export default function CodeEditor({ ); setIsValidating(false); }, 500); - }, [codeString, codeFile, dispatchOutput, stepIndex, chapterIndex]); + }, [codeString, codeFile, dispatchOutput, stepIndex, chapterIndex, setCodeString]); useValidationShortcut(handleValidate, codeString); useCodePersistence( @@ -241,6 +131,7 @@ export default function CodeEditor({ codeFile, ); + // Restore previous validation on lesson revisit const { isRestored } = useValidationRestore( chapterIndex, stepIndex, @@ -248,12 +139,13 @@ export default function CodeEditor({ setCodeString, ); + // Reset code to initial state const resetCode = () => { setCodeString(JSON.stringify(codeFile.code, null, 2)); dispatchOutput({ type: "RESET" }); }; - const handleEditorMount = (editor: monaco.editor.IStandaloneCodeEditor, monacoInstance: Monaco) => { + const handleEditorMount = (editor: any, monacoInstance: Monaco) => { setMonaco(monacoInstance); editorRef.current = editor; @@ -263,6 +155,7 @@ export default function CodeEditor({ return ( <> + {/* Show success banner when previous validation is restored */} {isRestored && (
{ + 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, + 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 }; +}; diff --git a/lib/client-functions.ts b/lib/client-functions.ts index c3c257d..26143ee 100644 --- a/lib/client-functions.ts +++ b/lib/client-functions.ts @@ -63,6 +63,7 @@ export async function validateCode( await hyperjumpCheckAnnotations(schemaCode, codeFile.expectedAnnotations); } + // Sort results: failed tests first, passed tests last const sortedResults = testCaseResults.sort((a, b) => { if (a.passed === b.passed) { return 0; @@ -70,7 +71,7 @@ export async function validateCode( return a.passed ? 1 : -1; }); - // Save validation result to localStorage BEFORE dispatching + // Persist validation results to localStorage for restoration on revisit saveValidationResult( chapterIndex, stepIndex, @@ -99,7 +100,7 @@ export async function validateCode( }); } } catch (e) { - // Save error state as well + // Persist error state for restoration on revisit saveValidationResult(chapterIndex, stepIndex, codeString, [], 0, "invalid"); if ((e as Error).message === "Invalid Schema") { @@ -208,6 +209,10 @@ export async function tryFormattingCode( } } +/** + * Restore previous validation results when revisiting a lesson + * Automatically restores both code and validation state from localStorage + */ export function restorePreviousValidation( chapterIndex: number, stepIndex: number, @@ -219,12 +224,12 @@ export function restorePreviousValidation( const validationResult = getValidationResult(chapterIndex, stepIndex); if (validationResult) { - // Restore code if setter provided + // Restore user's submitted code if (setCodeString) { setCodeString(validationResult.code); } - // Restore validation results + // Restore validation output state if (validationResult.validationStatus === "valid") { dispatchOutput({ type: "valid", diff --git a/lib/progressSaving.ts b/lib/progressSaving.ts index c856784..7e08394 100644 --- a/lib/progressSaving.ts +++ b/lib/progressSaving.ts @@ -1,3 +1,4 @@ +// Stores validation results for a specific lesson in localStorage interface ValidationResult { code: string; testCaseResults: any[]; @@ -8,6 +9,10 @@ interface ValidationResult { stepIndex: number; } +/** + * Save validation results to localStorage for a specific lesson + * This allows restoring validation state when user revisits the lesson + */ export function saveValidationResult( chapterIndex: number, stepIndex: number, @@ -38,6 +43,9 @@ export function saveValidationResult( } } +/** + * Retrieve saved validation results for a specific lesson + */ export function getValidationResult(chapterIndex: number, stepIndex: number): ValidationResult | null { if (typeof window === "undefined") return null; @@ -55,6 +63,9 @@ export function getValidationResult(chapterIndex: number, stepIndex: number): Va return null; } +/** + * Check if a validation result exists for a specific lesson + */ export function hasValidationResult(chapterIndex: number, stepIndex: number): boolean { if (typeof window === "undefined") return false; @@ -62,6 +73,9 @@ export function hasValidationResult(chapterIndex: number, stepIndex: number): bo return localStorage.getItem(key) !== null; } +/** + * Clear saved validation result for a specific lesson + */ export function clearValidationResult(chapterIndex: number, stepIndex: number): boolean { if (typeof window === "undefined") return false;