diff --git a/app/components/CodeEditor/CodeEditor.tsx b/app/components/CodeEditor/CodeEditor.tsx
index 32870c3..95d5b7b 100644
--- a/app/components/CodeEditor/CodeEditor.tsx
+++ b/app/components/CodeEditor/CodeEditor.tsx
@@ -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 = ({
@@ -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);
@@ -192,7 +120,7 @@ export default function CodeEditor({
);
setIsValidating(false);
}, 500);
- };
+ }, [codeString, codeFile, dispatchOutput, stepIndex, chapterIndex, setCodeString]);
useValidationShortcut(handleValidate, codeString);
useCodePersistence(
@@ -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 && (
+
+ ✅ Previous submission restored
+
+ )}
+
{
+ 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 b018af4..26143ee 100644
--- a/lib/client-functions.ts
+++ b/lib/client-functions.ts
@@ -8,6 +8,11 @@ import { CodeFile, TestCaseResult } from "./types";
import { hyperjumpCheckAnnotations, hyperjumpValidate } from "./validators";
import { sendGAEvent } from "@next/third-parties/google";
import { contentManager } from "./contentManager";
+import {
+ saveValidationResult,
+ getValidationResult,
+ hasValidationResult,
+} from "./progressSaving";
export async function validateCode(
codeString: string,
@@ -53,17 +58,30 @@ export async function validateCode(
});
}
}
+
if (codeFile.expectedAnnotations) {
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;
+ }
+ return a.passed ? 1 : -1;
+ });
+
+ // Persist validation results to localStorage for restoration on revisit
+ 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 +90,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 +100,9 @@ export async function validateCode(
});
}
} catch (e) {
+ // Persist error state for restoration on revisit
+ saveValidationResult(chapterIndex, stepIndex, codeString, [], 0, "invalid");
+
if ((e as Error).message === "Invalid Schema") {
dispatchOutput({
type: "invalidSchema",
@@ -193,3 +208,49 @@ export async function tryFormattingCode(
return;
}
}
+
+/**
+ * Restore previous validation results when revisiting a lesson
+ * Automatically restores both code and validation state from localStorage
+ */
+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 user's submitted code
+ if (setCodeString) {
+ setCodeString(validationResult.code);
+ }
+
+ // Restore validation output state
+ 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..7e08394 100644
--- a/lib/progressSaving.ts
+++ b/lib/progressSaving.ts
@@ -1,3 +1,89 @@
+// Stores validation results for a specific lesson in localStorage
+interface ValidationResult {
+ code: string;
+ testCaseResults: any[];
+ totalTestCases: number;
+ validationStatus: "valid" | "invalid" | "neutral";
+ timestamp: number;
+ chapterIndex: number;
+ 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,
+ 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;
+ }
+}
+
+/**
+ * Retrieve saved validation results for a specific lesson
+ */
+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;
+}
+
+/**
+ * Check if a validation result exists for a specific lesson
+ */
+export function hasValidationResult(chapterIndex: number, stepIndex: number): boolean {
+ if (typeof window === "undefined") return false;
+
+ const key = `validation-${chapterIndex}-${stepIndex}`;
+ 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;
+
+ const key = `validation-${chapterIndex}-${stepIndex}`;
+ localStorage.removeItem(key);
+ return true;
+}
+
export function setCheckpoint(path: string) {
if (typeof window === "undefined") return false;
const checkpoint = path;