Skip to content

Commit 86f428e

Browse files
committed
Feature, UX Improvement, and Bugfix(fix #123)
1 parent 1910a58 commit 86f428e

File tree

4 files changed

+163
-54
lines changed

4 files changed

+163
-54
lines changed

app/components/CodeEditor/CodeEditor.module.css

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
overflow-y: auto;
55
padding-bottom: 32px;
66
}
7+
78
.buttonsWrapper {
89
position: absolute;
910
bottom: 8px;
@@ -15,3 +16,36 @@
1516
padding-left: 16px;
1617
padding-right: 32px;
1718
}
19+
20+
21+
.tabBar {
22+
display: flex;
23+
background-color: var(--code-editor-background);
24+
border-bottom: 1px solid var(--border-color);
25+
padding: 0 10px;
26+
}
27+
28+
.tabButton {
29+
background: none;
30+
border: none;
31+
padding: 8px 15px;
32+
cursor: pointer;
33+
font-size: 14px;
34+
color: var(--text-muted);
35+
border-bottom: 3px solid transparent;
36+
transition: all 0.2s;
37+
margin-bottom: -1px;
38+
}
39+
40+
.tabButton:hover {
41+
color: var(--text-color);
42+
}
43+
44+
.activeTab {
45+
color: var(--primary-color) !important;
46+
border-bottom-color: var(--primary-color);
47+
}
48+
49+
.codeEditor {
50+
flex: 1;
51+
}

app/components/CodeEditor/CodeEditor.tsx

Lines changed: 73 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@ import { CodeFile, OutputResult } from "@/lib/types";
1616
import { OutputReducerAction } from "@/lib/reducers";
1717
import CertificateButton from "../CertificateButton/CertificateButton";
1818

19-
// Custom hook for editor theme setup
2019
const useEditorTheme = (monaco: Monaco, colorMode: "dark" | "light") => {
2120
useEffect(() => {
2221
if (monaco) {
@@ -33,7 +32,6 @@ const useEditorTheme = (monaco: Monaco, colorMode: "dark" | "light") => {
3332
}, [monaco, colorMode]);
3433
};
3534

36-
// Custom hook for keyboard shortcuts
3735
const useValidationShortcut = (
3836
handleValidate: () => void,
3937
codeString: string,
@@ -56,7 +54,6 @@ const useValidationShortcut = (
5654
}, [handleValidate, codeString]);
5755
};
5856

59-
// Custom hook for code persistence
6057
const useCodePersistence = (
6158
chapterIndex: number,
6259
stepIndex: number,
@@ -66,7 +63,6 @@ const useCodePersistence = (
6663
) => {
6764
const userSolutionStore = useUserSolutionStore();
6865

69-
// Load saved code
7066
useEffect(() => {
7167
const savedCode = userSolutionStore.getSavedUserSolutionByLesson(
7268
chapterIndex,
@@ -77,24 +73,16 @@ const useCodePersistence = (
7773
}
7874
}, [chapterIndex, stepIndex]);
7975

80-
// Save code changes
81-
useEffect(() => {
82-
userSolutionStore.saveUserSolutionForLesson(
83-
chapterIndex,
84-
stepIndex,
85-
codeString,
86-
);
87-
}, [codeString, chapterIndex, stepIndex]);
8876

89-
// Initialize code if no saved solutions
9077
useEffect(() => {
9178
if (Object.keys(userSolutionStore.userSolutionsByLesson).length === 0) {
9279
setCodeString(JSON.stringify(codeFile.code, null, 2));
9380
}
9481
}, [userSolutionStore]);
82+
83+
return userSolutionStore;
9584
};
9685

97-
// EditorControls component for the buttons section
9886
const EditorControls = ({
9987
handleValidate,
10088
isValidating,
@@ -160,6 +148,9 @@ export default function CodeEditor({
160148
stepIndex,
161149
chapterIndex,
162150
outputResult,
151+
solutionRequested,
152+
resetSolution,
153+
hasValidated,
163154
}: {
164155
codeString: string;
165156
setCodeString: (codeString: string) => void;
@@ -169,16 +160,28 @@ export default function CodeEditor({
169160
stepIndex: number;
170161
chapterIndex: number;
171162
outputResult: OutputResult;
163+
solutionRequested: boolean;
164+
resetSolution: () => void;
165+
hasValidated: boolean;
172166
}) {
173167
const { colorMode } = useColorMode();
174168
const [monaco, setMonaco] = useState<any>(null);
175169
const [isValidating, setIsValidating] = useState(false);
176170
const editorStore = useEditorStore();
177171
const editorRef = useRef<any>(null);
178172

179-
// Apply custom hooks
173+
const [activeView, setActiveView] = useState<'code' | 'solution'>('code');
174+
180175
useEditorTheme(monaco, colorMode);
181176

177+
const userSolutionStore = useCodePersistence(
178+
chapterIndex,
179+
stepIndex,
180+
codeString,
181+
setCodeString,
182+
codeFile,
183+
);
184+
182185
const handleValidate = () => {
183186
setIsValidating(true);
184187
setTimeout(() => {
@@ -195,42 +198,84 @@ export default function CodeEditor({
195198
};
196199

197200
useValidationShortcut(handleValidate, codeString);
198-
useCodePersistence(
199-
chapterIndex,
200-
stepIndex,
201-
codeString,
202-
setCodeString,
203-
codeFile,
204-
);
205201

206202
const resetCode = () => {
207-
setCodeString(JSON.stringify(codeFile.code, null, 2));
203+
const initialCode = JSON.stringify(codeFile.code, null, 2);
204+
setCodeString(initialCode);
208205
dispatchOutput({ type: "RESET" });
206+
207+
resetSolution();
208+
setActiveView('code');
209+
210+
userSolutionStore.saveUserSolutionForLesson(chapterIndex, stepIndex, initialCode);
209211
};
210212

211213
const handleEditorMount = (editor: any, monaco: Monaco) => {
212214
setMonaco(monaco);
213-
214215
editorRef.current = editor;
215216
editorStore.setEditor(editor);
216217
editorStore.setMonaco(monaco);
217218
};
218219

220+
const handleCodeChange = (newCode: string | undefined) => {
221+
if (activeView === 'code' && newCode !== undefined) {
222+
setCodeString(newCode);
223+
userSolutionStore.saveUserSolutionForLesson(
224+
chapterIndex,
225+
stepIndex,
226+
newCode
227+
);
228+
}
229+
};
230+
231+
useEffect(() => {
232+
if (solutionRequested && activeView !== 'solution') {
233+
setActiveView('solution');
234+
}
235+
if (!solutionRequested && activeView === 'solution') {
236+
setActiveView('code');
237+
}
238+
}, [solutionRequested]);
239+
240+
const isSolutionView = activeView === 'solution';
241+
const editorContent = isSolutionView
242+
? JSON.stringify(codeFile.solution, null, 2)
243+
: codeString;
244+
219245
return (
220246
<>
247+
<div className={styles.tabBar}>
248+
<button
249+
className={ctx(styles.tabButton, activeView === 'code' && styles.activeTab)}
250+
onClick={() => setActiveView('code')}
251+
>
252+
My Code
253+
</button>
254+
255+
{solutionRequested && (
256+
<button
257+
className={ctx(styles.tabButton, activeView === 'solution' && styles.activeTab)}
258+
onClick={() => setActiveView('solution')}
259+
>
260+
Solution
261+
</button>
262+
)}
263+
</div>
264+
221265
<div className={ctx(styles.codeEditor, GeistMono.className)}>
222266
<Editor
223267
language="json"
224-
defaultValue={codeString}
268+
value={editorContent}
269+
key={activeView}
225270
theme={colorMode === "light" ? "light" : "my-theme"}
226-
value={codeString}
227271
height="100%"
228-
onChange={(codeString) => setCodeString(codeString ?? "")}
272+
onChange={handleCodeChange}
229273
options={{
230274
minimap: { enabled: false },
231275
fontSize: 14,
232276
formatOnPaste: true,
233277
formatOnType: true,
278+
readOnly: isSolutionView,
234279
}}
235280
onMount={handleEditorMount}
236281
/>
@@ -244,4 +289,4 @@ export default function CodeEditor({
244289
/>
245290
</>
246291
);
247-
}
292+
}

app/components/EditorNOutput/EditorNOutput.tsx

Lines changed: 31 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -23,16 +23,29 @@ export default function EditorNOutput({
2323
JSON.stringify(codeFile.code, null, 2),
2424
);
2525

26+
// Tracks if the user has requested to view the solution
27+
const [solutionRequested, setSolutionRequested] = useState(false);
28+
29+
// NEW STATE: Tracks if the user has validated at least once
30+
const [hasValidated, setHasValidated] = useState(false);
31+
32+
// Function to show solution
2633
const showSolution = () => {
27-
setCodeString(JSON.stringify(codeFile.solution, null, 2));
34+
setSolutionRequested(true);
35+
};
36+
37+
// Function to reset the solution visibility state
38+
const resetSolution = () => {
39+
setSolutionRequested(false);
40+
setHasValidated(false);
2841
};
2942

3043
const [output, dispatchOutput] = useReducer(outputReducer, {
3144
validityStatus: "neutral",
3245
errors: "",
3346
testCaseResults: [],
3447
});
35-
const [topWidth, setTopWidth] = useState(400); // Initial width of the left div
48+
const [topWidth, setTopWidth] = useState(400);
3649
const dividerRef = useRef<HTMLDivElement>(null);
3750

3851
const containerRef = useRef<HTMLDivElement>(null);
@@ -70,6 +83,12 @@ export default function EditorNOutput({
7083
}
7184
}, []);
7285

86+
useEffect(() => {
87+
if (output.validityStatus !== "neutral") {
88+
setHasValidated(true);
89+
}
90+
}, [output.validityStatus]);
91+
7392
return (
7493
<div
7594
className={styles.codeEditorNOutput}
@@ -93,6 +112,9 @@ export default function EditorNOutput({
93112
stepIndex={stepIndex}
94113
chapterIndex={chapterIndex}
95114
outputResult={output}
115+
solutionRequested={solutionRequested}
116+
resetSolution={resetSolution}
117+
hasValidated={hasValidated}
96118
/>
97119
</Box>
98120
<div
@@ -104,8 +126,13 @@ export default function EditorNOutput({
104126
className={styles.outputWrapper}
105127
style={{ height: `calc(100% - ${topWidth}px - 6px)` }}
106128
>
107-
<Output outputResult={output} showSolution={showSolution} />
129+
<Output
130+
outputResult={output}
131+
showSolution={showSolution}
132+
solutionRequested={solutionRequested}
133+
hasValidated={hasValidated}
134+
/>
108135
</div>
109136
</div>
110137
);
111-
}
138+
}

app/components/Output/Output.tsx

Lines changed: 25 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -81,9 +81,13 @@ const SchemaError = ({ schemaPath }: { schemaPath: string }) => {
8181
function Output({
8282
outputResult,
8383
showSolution,
84+
solutionRequested,
85+
hasValidated,
8486
}: {
8587
outputResult: OutputResult;
8688
showSolution: () => void;
89+
solutionRequested: boolean;
90+
hasValidated: boolean;
8791
}) {
8892
let outputBodyContent;
8993

@@ -92,7 +96,7 @@ function Output({
9296
<Flex dir="row" gap={1} paddingTop={2}>
9397
{" "}
9498
Please click the{" "}
95-
<MyBtn variant="default" onClick={() => {}}>
99+
<MyBtn variant="default" onClick={() => { }}>
96100
validate
97101
</MyBtn>{" "}
98102
button or use <KeyBindings keys={["Shift", "Enter"]} /> to view the
@@ -149,29 +153,28 @@ function Output({
149153

150154
<div className={classnames(styles.outputBody)}>
151155
{outputBodyContent}
152-
{outputResult.validityStatus !== "neutral" &&
153-
outputResult.validityStatus !== "valid" && (
154-
<div className={styles.footer}>
155-
Stuck?{" "}
156-
<button
157-
onClick={() => {
158-
showSolution();
159-
sendGAEvent("event", "buttonClicked", {
160-
value: "View Solution",
161-
});
162-
}}
163-
style={{
164-
color: "hsl(var(--link-color))",
165-
textDecoration: "underline",
166-
}}
167-
>
168-
View Solution
169-
</button>
170-
</div>
171-
)}
156+
{hasValidated && !solutionRequested && (
157+
<div className={styles.footer}>
158+
Stuck?{" "}
159+
<button
160+
onClick={() => {
161+
showSolution();
162+
sendGAEvent("event", "buttonClicked", {
163+
value: "View Solution",
164+
});
165+
}}
166+
style={{
167+
color: "hsl(var(--link-color))",
168+
textDecoration: "underline",
169+
}}
170+
>
171+
View Solution
172+
</button>
173+
</div>
174+
)}
172175
</div>
173176
</>
174177
);
175178
}
176179

177-
export default Output;
180+
export default Output;

0 commit comments

Comments
 (0)