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
34 changes: 34 additions & 0 deletions app/components/CodeEditor/CodeEditor.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
overflow-y: auto;
padding-bottom: 32px;
}

.buttonsWrapper {
position: absolute;
bottom: 8px;
Expand All @@ -15,3 +16,36 @@
padding-left: 16px;
padding-right: 32px;
}


.tabBar {
display: flex;
background-color: var(--code-editor-background);
border-bottom: 1px solid var(--border-color);
padding: 0 10px;
}

.tabButton {
background: none;
border: none;
padding: 8px 15px;
cursor: pointer;
font-size: 14px;
color: var(--text-muted);
border-bottom: 3px solid transparent;
transition: all 0.2s;
margin-bottom: -1px;
}

.tabButton:hover {
color: var(--text-color);
}

.activeTab {
color: var(--primary-color) !important;
border-bottom-color: var(--primary-color);
}

.codeEditor {
flex: 1;
}
101 changes: 73 additions & 28 deletions app/components/CodeEditor/CodeEditor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@ 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) {
Expand All @@ -33,7 +32,6 @@ const useEditorTheme = (monaco: Monaco, colorMode: "dark" | "light") => {
}, [monaco, colorMode]);
};

// Custom hook for keyboard shortcuts
const useValidationShortcut = (
handleValidate: () => void,
codeString: string,
Expand All @@ -56,7 +54,6 @@ const useValidationShortcut = (
}, [handleValidate, codeString]);
};

// Custom hook for code persistence
const useCodePersistence = (
chapterIndex: number,
stepIndex: number,
Expand All @@ -66,7 +63,6 @@ const useCodePersistence = (
) => {
const userSolutionStore = useUserSolutionStore();

// Load saved code
useEffect(() => {
const savedCode = userSolutionStore.getSavedUserSolutionByLesson(
chapterIndex,
Expand All @@ -77,24 +73,16 @@ const useCodePersistence = (
}
}, [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]);

return userSolutionStore;
};

// EditorControls component for the buttons section
const EditorControls = ({
handleValidate,
isValidating,
Expand Down Expand Up @@ -160,6 +148,9 @@ export default function CodeEditor({
stepIndex,
chapterIndex,
outputResult,
solutionRequested,
resetSolution,
hasValidated,
}: {
codeString: string;
setCodeString: (codeString: string) => void;
Expand All @@ -169,16 +160,28 @@ export default function CodeEditor({
stepIndex: number;
chapterIndex: number;
outputResult: OutputResult;
solutionRequested: boolean;
resetSolution: () => void;
hasValidated: boolean;
}) {
const { colorMode } = useColorMode();
const [monaco, setMonaco] = useState<any>(null);
const [isValidating, setIsValidating] = useState(false);
const editorStore = useEditorStore();
const editorRef = useRef<any>(null);

// Apply custom hooks
const [activeView, setActiveView] = useState<'code' | 'solution'>('code');

useEditorTheme(monaco, colorMode);

const userSolutionStore = useCodePersistence(
chapterIndex,
stepIndex,
codeString,
setCodeString,
codeFile,
);

const handleValidate = () => {
setIsValidating(true);
setTimeout(() => {
Expand All @@ -195,42 +198,84 @@ export default function CodeEditor({
};

useValidationShortcut(handleValidate, codeString);
useCodePersistence(
chapterIndex,
stepIndex,
codeString,
setCodeString,
codeFile,
);

const resetCode = () => {
setCodeString(JSON.stringify(codeFile.code, null, 2));
const initialCode = JSON.stringify(codeFile.code, null, 2);
setCodeString(initialCode);
dispatchOutput({ type: "RESET" });

resetSolution();
setActiveView('code');

userSolutionStore.saveUserSolutionForLesson(chapterIndex, stepIndex, initialCode);
};

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

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

const handleCodeChange = (newCode: string | undefined) => {
if (activeView === 'code' && newCode !== undefined) {
setCodeString(newCode);
userSolutionStore.saveUserSolutionForLesson(
chapterIndex,
stepIndex,
newCode
);
}
};

useEffect(() => {
if (solutionRequested && activeView !== 'solution') {
setActiveView('solution');
}
if (!solutionRequested && activeView === 'solution') {
setActiveView('code');
}
}, [solutionRequested]);

const isSolutionView = activeView === 'solution';
const editorContent = isSolutionView
? JSON.stringify(codeFile.solution, null, 2)
: codeString;

return (
<>
<div className={styles.tabBar}>
<button
className={ctx(styles.tabButton, activeView === 'code' && styles.activeTab)}
onClick={() => setActiveView('code')}
>
My Code
</button>

{solutionRequested && (
<button
className={ctx(styles.tabButton, activeView === 'solution' && styles.activeTab)}
onClick={() => setActiveView('solution')}
>
Solution
</button>
)}
</div>

<div className={ctx(styles.codeEditor, GeistMono.className)}>
<Editor
language="json"
defaultValue={codeString}
value={editorContent}
key={activeView}
theme={colorMode === "light" ? "light" : "my-theme"}
value={codeString}
height="100%"
onChange={(codeString) => setCodeString(codeString ?? "")}
onChange={handleCodeChange}
options={{
minimap: { enabled: false },
fontSize: 14,
formatOnPaste: true,
formatOnType: true,
readOnly: isSolutionView,
}}
onMount={handleEditorMount}
/>
Expand All @@ -244,4 +289,4 @@ export default function CodeEditor({
/>
</>
);
}
}
35 changes: 31 additions & 4 deletions app/components/EditorNOutput/EditorNOutput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,16 +23,29 @@ export default function EditorNOutput({
JSON.stringify(codeFile.code, null, 2),
);

// Tracks if the user has requested to view the solution
const [solutionRequested, setSolutionRequested] = useState(false);

// NEW STATE: Tracks if the user has validated at least once
const [hasValidated, setHasValidated] = useState(false);

// Function to show solution
const showSolution = () => {
setCodeString(JSON.stringify(codeFile.solution, null, 2));
setSolutionRequested(true);
};

// Function to reset the solution visibility state
const resetSolution = () => {
setSolutionRequested(false);
setHasValidated(false);
};

const [output, dispatchOutput] = useReducer(outputReducer, {
validityStatus: "neutral",
errors: "",
testCaseResults: [],
});
const [topWidth, setTopWidth] = useState(400); // Initial width of the left div
const [topWidth, setTopWidth] = useState(400);
const dividerRef = useRef<HTMLDivElement>(null);

const containerRef = useRef<HTMLDivElement>(null);
Expand Down Expand Up @@ -70,6 +83,12 @@ export default function EditorNOutput({
}
}, []);

useEffect(() => {
if (output.validityStatus !== "neutral") {
setHasValidated(true);
}
}, [output.validityStatus]);

return (
<div
className={styles.codeEditorNOutput}
Expand All @@ -93,6 +112,9 @@ export default function EditorNOutput({
stepIndex={stepIndex}
chapterIndex={chapterIndex}
outputResult={output}
solutionRequested={solutionRequested}
resetSolution={resetSolution}
hasValidated={hasValidated}
/>
</Box>
<div
Expand All @@ -104,8 +126,13 @@ export default function EditorNOutput({
className={styles.outputWrapper}
style={{ height: `calc(100% - ${topWidth}px - 6px)` }}
>
<Output outputResult={output} showSolution={showSolution} />
<Output
outputResult={output}
showSolution={showSolution}
solutionRequested={solutionRequested}
hasValidated={hasValidated}
/>
</div>
</div>
);
}
}
47 changes: 25 additions & 22 deletions app/components/Output/Output.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -81,9 +81,13 @@ const SchemaError = ({ schemaPath }: { schemaPath: string }) => {
function Output({
outputResult,
showSolution,
solutionRequested,
hasValidated,
}: {
outputResult: OutputResult;
showSolution: () => void;
solutionRequested: boolean;
hasValidated: boolean;
}) {
let outputBodyContent;

Expand All @@ -92,7 +96,7 @@ function Output({
<Flex dir="row" gap={1} paddingTop={2}>
{" "}
Please click the{" "}
<MyBtn variant="default" onClick={() => {}}>
<MyBtn variant="default" onClick={() => { }}>
validate
</MyBtn>{" "}
button or use <KeyBindings keys={["Shift", "Enter"]} /> to view the
Expand Down Expand Up @@ -149,29 +153,28 @@ function Output({

<div className={classnames(styles.outputBody)}>
{outputBodyContent}
{outputResult.validityStatus !== "neutral" &&
outputResult.validityStatus !== "valid" && (
<div className={styles.footer}>
Stuck?{" "}
<button
onClick={() => {
showSolution();
sendGAEvent("event", "buttonClicked", {
value: "View Solution",
});
}}
style={{
color: "hsl(var(--link-color))",
textDecoration: "underline",
}}
>
View Solution
</button>
</div>
)}
{hasValidated && !solutionRequested && (
<div className={styles.footer}>
Stuck?{" "}
<button
onClick={() => {
showSolution();
sendGAEvent("event", "buttonClicked", {
value: "View Solution",
});
}}
style={{
color: "hsl(var(--link-color))",
textDecoration: "underline",
}}
>
View Solution
</button>
</div>
)}
</div>
</>
);
}

export default Output;
export default Output;