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
74 changes: 67 additions & 7 deletions app/components/CodeEditor/CodeEditor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -94,6 +99,39 @@ const useCodePersistence = (
}, [userSolutionStore]);
};

// Custom hook for validation restoration
const useValidationRestore = (
chapterIndex: number,
stepIndex: number,
dispatchOutput: React.Dispatch<OutputReducerAction>,
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,
Expand Down Expand Up @@ -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);
Expand All @@ -192,7 +230,7 @@ export default function CodeEditor({
);
setIsValidating(false);
}, 500);
};
}, [codeString, codeFile, dispatchOutput, stepIndex, chapterIndex]);

useValidationShortcut(handleValidate, codeString);
useCodePersistence(
Expand All @@ -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 && (
<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
82 changes: 69 additions & 13 deletions lib/client-functions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -53,17 +58,29 @@ export async function validateCode(
});
}
}

if (codeFile.expectedAnnotations) {
await hyperjumpCheckAnnotations(schemaCode, codeFile.expectedAnnotations);
}

const sortedResults = testCaseResults.sort((a, b) => {
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 },
Expand All @@ -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 },
Expand All @@ -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",
Expand Down Expand Up @@ -193,3 +207,45 @@ export async function tryFormattingCode(
return;
}
}

export function restorePreviousValidation(
chapterIndex: number,
stepIndex: number,
dispatchOutput: React.Dispatch<OutputReducerAction>,
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";
72 changes: 72 additions & 0 deletions lib/progressSaving.ts
Original file line number Diff line number Diff line change
@@ -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;
Expand Down