Skip to content

Commit 2753ddb

Browse files
davidagustinclaude
andcommitted
feat: add Monaco Editor everywhere, language search, and settings menu
- Replace all textareas/pre blocks with Monaco Editor in exercises - Add dynamic JS/TS language switching in drill mode based on problem source - Add searchable language grid on homepage with grid/list view toggle - Add settings menu with clear data dialog - Add PHP and Kotlin languages to homepage - Add JS/TS subtitle indicators on language cards - Update README with new features - Fix lint-staged config to not run biome on markdown files Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent 98e1382 commit 2753ddb

File tree

10 files changed

+1034
-135
lines changed

10 files changed

+1034
-135
lines changed

README.md

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ Learning programming syntax is like learning vocabulary in a new language—you
2828

2929
## The Solution
3030

31-
**Coding Drills** transforms syntax learning into an interactive, game-like experience. Practice typing real code, test your knowledge with timed quizzes, and build lasting muscle memory across **9 programming languages**.
31+
**Coding Drills** transforms syntax learning into an interactive, game-like experience. Practice typing real code, test your knowledge with timed quizzes, and build lasting muscle memory across **11 programming languages**.
3232

3333
---
3434

@@ -38,12 +38,12 @@ Learning programming syntax is like learning vocabulary in a new language—you
3838

3939
| Mode | Description |
4040
|------|-------------|
41-
| **Drill Mode** | Type code solutions to method challenges. Real-time execution for JS/TS with anti-hardcoding validation. |
41+
| **Drill Mode** | Type code solutions with Monaco Editor (syntax highlighting, IntelliSense). Real-time execution for JS/TS with scoring based on speed and streaks. |
4242
| **Quiz Mode** | Timed card selection matching inputs to methods. Scoring system with streaks and leaderboards. |
4343
| **Algorithm Exercises** | Master traversal patterns (DFS, BFS), recursion, prime generation, Fibonacci, and iteration control. |
4444
| **Method Reference** | Browse 717+ methods with full documentation, examples, and complexity analysis. |
4545

46-
### 🌍 Nine Languages Supported
46+
### 🌍 Eleven Languages Supported
4747

4848
<div align="center">
4949

@@ -52,6 +52,7 @@ Learning programming syntax is like learning vocabulary in a new language—you
5252
| ![JavaScript](https://img.shields.io/badge/JavaScript-F7DF1E?style=flat-square&logo=javascript&logoColor=black) | ![TypeScript](https://img.shields.io/badge/TypeScript-3178C6?style=flat-square&logo=typescript&logoColor=white) | ![Python](https://img.shields.io/badge/Python-3776AB?style=flat-square&logo=python&logoColor=white) |
5353
| ![Java](https://img.shields.io/badge/Java-ED8B00?style=flat-square&logo=openjdk&logoColor=white) | ![C++](https://img.shields.io/badge/C++-00599C?style=flat-square&logo=cplusplus&logoColor=white) | ![C#](https://img.shields.io/badge/C%23-239120?style=flat-square&logo=csharp&logoColor=white) |
5454
| ![Go](https://img.shields.io/badge/Go-00ADD8?style=flat-square&logo=go&logoColor=white) | ![Ruby](https://img.shields.io/badge/Ruby-CC342D?style=flat-square&logo=ruby&logoColor=white) | ![C](https://img.shields.io/badge/C-A8B9CC?style=flat-square&logo=c&logoColor=black) |
55+
| ![PHP](https://img.shields.io/badge/PHP-777BB4?style=flat-square&logo=php&logoColor=white) | ![Kotlin](https://img.shields.io/badge/Kotlin-7F52FF?style=flat-square&logo=kotlin&logoColor=white) | |
5556

5657
</div>
5758

@@ -86,6 +87,7 @@ str.split(" ").join("-")
8687
| **Framework** | Next.js 16 (App Router) |
8788
| **Language** | TypeScript 5 |
8889
| **Styling** | Tailwind CSS 4 |
90+
| **Code Editor** | Monaco Editor (VS Code's editor) |
8991
| **Testing** | Playwright (3,000+ lines of E2E tests) |
9092
| **State** | React Context + localStorage |
9193
| **Code Execution** | Browser-based JS execution, pattern matching for compiled languages |
@@ -248,11 +250,14 @@ git push origin feature/add-rust-support
248250

249251
## Roadmap
250252

251-
- [ ] **Monaco Editor** - Syntax highlighting and IntelliSense in drill mode
253+
- [x] **Monaco Editor** - Syntax highlighting and IntelliSense in drill mode
254+
- [x] **Scoring System** - Points based on speed, difficulty, and streaks
255+
- [x] **JS/TS Toggle** - Practice JavaScript and TypeScript together
256+
- [x] **Data Management** - Clear saved data with selective deletion dialog
252257
- [ ] **User Accounts** - Cloud sync for progress across devices
253258
- [ ] **Spaced Repetition** - Smart review scheduling based on performance
254259
- [ ] **Multiplayer Mode** - Race against friends in real-time
255-
- [ ] **More Languages** - Rust, Kotlin, Swift, PHP, Scala
260+
- [ ] **More Languages** - Rust, Swift, Scala
256261
- [ ] **Mobile App** - Native iOS/Android apps
257262
- [ ] **API** - Public API for integration with other learning platforms
258263

@@ -264,7 +269,7 @@ git push origin feature/add-rust-support
264269

265270
| Metric | Count |
266271
|--------|-------|
267-
| **Languages** | 9 |
272+
| **Languages** | 11 |
268273
| **Coding Problems** | 450+ |
269274
| **Method References** | 717 |
270275
| **Algorithm Exercises** | 15+ per language |

app/[language]/SettingsMenu.tsx

Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
'use client';
2+
3+
import { useCallback, useEffect, useRef, useState } from 'react';
4+
import ClearDataDialog from '@/components/ClearDataDialog';
5+
6+
export function SettingsMenu() {
7+
const [isMenuOpen, setIsMenuOpen] = useState(false);
8+
const [isClearDataDialogOpen, setIsClearDataDialogOpen] = useState(false);
9+
const menuRef = useRef<HTMLDivElement>(null);
10+
const buttonRef = useRef<HTMLButtonElement>(null);
11+
12+
// Close menu when clicking outside
13+
useEffect(() => {
14+
if (!isMenuOpen) return;
15+
16+
const handleClickOutside = (e: MouseEvent) => {
17+
if (
18+
menuRef.current &&
19+
!menuRef.current.contains(e.target as Node) &&
20+
buttonRef.current &&
21+
!buttonRef.current.contains(e.target as Node)
22+
) {
23+
setIsMenuOpen(false);
24+
}
25+
};
26+
27+
const handleEscape = (e: KeyboardEvent) => {
28+
if (e.key === 'Escape') {
29+
setIsMenuOpen(false);
30+
}
31+
};
32+
33+
document.addEventListener('mousedown', handleClickOutside);
34+
document.addEventListener('keydown', handleEscape);
35+
36+
return () => {
37+
document.removeEventListener('mousedown', handleClickOutside);
38+
document.removeEventListener('keydown', handleEscape);
39+
};
40+
}, [isMenuOpen]);
41+
42+
const toggleMenu = useCallback(() => {
43+
setIsMenuOpen((prev) => !prev);
44+
}, []);
45+
46+
const openClearDataDialog = useCallback(() => {
47+
setIsMenuOpen(false);
48+
setIsClearDataDialogOpen(true);
49+
}, []);
50+
51+
const closeClearDataDialog = useCallback(() => {
52+
setIsClearDataDialogOpen(false);
53+
}, []);
54+
55+
return (
56+
<>
57+
<div className="relative">
58+
{/* Settings Button */}
59+
<button
60+
ref={buttonRef}
61+
type="button"
62+
onClick={toggleMenu}
63+
className="p-2 text-zinc-400 hover:text-white hover:bg-zinc-800 rounded-lg transition-colors cursor-pointer"
64+
aria-label="Settings"
65+
aria-expanded={isMenuOpen}
66+
aria-haspopup="menu"
67+
>
68+
<svg
69+
className="w-5 h-5"
70+
fill="none"
71+
stroke="currentColor"
72+
viewBox="0 0 24 24"
73+
aria-hidden="true"
74+
>
75+
<path
76+
strokeLinecap="round"
77+
strokeLinejoin="round"
78+
strokeWidth={2}
79+
d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z"
80+
/>
81+
<path
82+
strokeLinecap="round"
83+
strokeLinejoin="round"
84+
strokeWidth={2}
85+
d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"
86+
/>
87+
</svg>
88+
</button>
89+
90+
{/* Dropdown Menu */}
91+
{isMenuOpen && (
92+
<div
93+
ref={menuRef}
94+
role="menu"
95+
className="absolute right-0 mt-2 w-56 bg-zinc-900 border border-zinc-800 rounded-lg shadow-lg overflow-hidden z-50"
96+
>
97+
<div className="py-1">
98+
<button
99+
type="button"
100+
role="menuitem"
101+
onClick={openClearDataDialog}
102+
className="w-full px-4 py-2.5 text-left text-sm text-zinc-300 hover:bg-zinc-800 hover:text-white transition-colors flex items-center gap-3 cursor-pointer"
103+
>
104+
<svg
105+
className="w-4 h-4 text-red-400"
106+
fill="none"
107+
stroke="currentColor"
108+
viewBox="0 0 24 24"
109+
aria-hidden="true"
110+
>
111+
<path
112+
strokeLinecap="round"
113+
strokeLinejoin="round"
114+
strokeWidth={2}
115+
d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"
116+
/>
117+
</svg>
118+
<span>Clear Saved Data</span>
119+
</button>
120+
</div>
121+
</div>
122+
)}
123+
</div>
124+
125+
{/* Clear Data Dialog */}
126+
<ClearDataDialog isOpen={isClearDataDialogOpen} onClose={closeClearDataDialog} />
127+
</>
128+
);
129+
}
130+
131+
export default SettingsMenu;

app/[language]/drill/page.tsx

Lines changed: 51 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
'use client';
22

33
import { useParams, useRouter } from 'next/navigation';
4-
import { useCallback, useEffect, useRef, useState } from 'react';
4+
import { useCallback, useEffect, useState } from 'react';
5+
import CodeEditor from '@/components/CodeEditor';
56
import { QuestionCountSlider } from '@/components/QuestionCountSlider';
67
import { formatOutput, validateProblemAnswer } from '@/lib/codeValidator';
78
import { problemsByLanguage } from '@/lib/problems/index';
@@ -30,6 +31,11 @@ const SIBLING_LANGUAGES: Partial<Record<LanguageId, LanguageId>> = {
3031
typescript: 'javascript',
3132
};
3233

34+
// Extended problem type that tracks source language
35+
interface ProblemWithLanguage extends Problem {
36+
sourceLanguage: LanguageId;
37+
}
38+
3339
interface DrillState {
3440
currentIndex: number;
3541
answers: AnswerRecord[];
@@ -41,7 +47,7 @@ interface DrillState {
4147
}
4248

4349
interface AnswerRecord {
44-
problem: Problem;
50+
problem: ProblemWithLanguage;
4551
userAnswer: string;
4652
isCorrect: boolean;
4753
error?: string;
@@ -207,14 +213,25 @@ function getCategories(language: LanguageId, includeSibling = false): string[] {
207213
return Array.from(categories).sort();
208214
}
209215

210-
function selectProblems(language: LanguageId, config: DrillConfig): Problem[] {
211-
let problems = PROBLEMS_BY_LANGUAGE[language] || [];
216+
function selectProblems(language: LanguageId, config: DrillConfig): ProblemWithLanguage[] {
217+
// Tag problems with their source language
218+
const baseProblems: ProblemWithLanguage[] = (PROBLEMS_BY_LANGUAGE[language] || []).map((p) => ({
219+
...p,
220+
sourceLanguage: language,
221+
}));
222+
223+
let problems: ProblemWithLanguage[] = baseProblems;
212224

213225
// Include sibling language problems if requested
214226
if (config.includeSiblingLanguage) {
215227
const siblingLang = SIBLING_LANGUAGES[language];
216228
if (siblingLang) {
217-
const siblingProblems = PROBLEMS_BY_LANGUAGE[siblingLang] || [];
229+
const siblingProblems: ProblemWithLanguage[] = (PROBLEMS_BY_LANGUAGE[siblingLang] || []).map(
230+
(p) => ({
231+
...p,
232+
sourceLanguage: siblingLang,
233+
}),
234+
);
218235
problems = [...problems, ...siblingProblems];
219236
}
220237
}
@@ -702,7 +719,7 @@ function SetupPhase({ language, onStart }: SetupPhaseProps) {
702719
}
703720

704721
interface DrillPhaseProps {
705-
problems: Problem[];
722+
problems: ProblemWithLanguage[];
706723
state: DrillState;
707724
language: LanguageId;
708725
onAnswer: (answer: string) => void;
@@ -713,14 +730,14 @@ interface DrillPhaseProps {
713730
function DrillPhaseComponent({
714731
problems,
715732
state,
733+
language,
716734
onAnswer,
717735
onSkip,
718736
questionStartTime,
719737
}: DrillPhaseProps) {
720738
const [userAnswer, setUserAnswer] = useState('');
721739
const [elapsedTime, setElapsedTime] = useState(0);
722740
const [questionTime, setQuestionTime] = useState(0);
723-
const inputRef = useRef<HTMLTextAreaElement>(null);
724741
const currentProblem = problems[state.currentIndex];
725742

726743
// Total time timer effect
@@ -742,24 +759,12 @@ function DrillPhaseComponent({
742759
return () => clearInterval(interval);
743760
}, [questionStartTime, state.currentIndex]);
744761

745-
// Auto-focus on input
746-
useEffect(() => {
747-
inputRef.current?.focus();
748-
}, []);
749-
750-
const handleSubmit = () => {
762+
const handleSubmit = useCallback(() => {
751763
if (userAnswer.trim()) {
752764
onAnswer(userAnswer.trim());
753765
setUserAnswer('');
754766
}
755-
};
756-
757-
const handleKeyDown = (e: React.KeyboardEvent) => {
758-
if (e.key === 'Enter' && (e.metaKey || e.ctrlKey)) {
759-
e.preventDefault();
760-
handleSubmit();
761-
}
762-
};
767+
}, [userAnswer, onAnswer]);
763768

764769
const difficultyColors = {
765770
easy: 'bg-green-500/20 text-green-400 border-green-500/30',
@@ -842,17 +847,30 @@ function DrillPhaseComponent({
842847

843848
{/* Answer Input */}
844849
<div className="p-4">
845-
<label htmlFor="answer-input" className="block text-sm font-medium text-zinc-300 mb-2">
846-
Your Answer
847-
</label>
848-
<textarea
849-
id="answer-input"
850-
ref={inputRef}
851-
value={userAnswer}
852-
onChange={(e) => setUserAnswer(e.target.value)}
853-
onKeyDown={handleKeyDown}
854-
placeholder="Type your expression here..."
855-
className="w-full h-24 px-4 py-3 font-mono text-sm bg-zinc-800 border border-zinc-700 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent resize-none text-zinc-100 placeholder-zinc-500"
850+
<div className="flex items-center justify-between mb-2">
851+
<span className="text-sm font-medium text-zinc-300">Your Answer</span>
852+
{currentProblem.sourceLanguage !== language && (
853+
<span
854+
className={`text-xs px-2 py-0.5 rounded ${
855+
currentProblem.sourceLanguage === 'typescript'
856+
? 'bg-blue-500/20 text-blue-400'
857+
: 'bg-yellow-500/20 text-yellow-400'
858+
}`}
859+
>
860+
{currentProblem.sourceLanguage === 'typescript' ? 'TypeScript' : 'JavaScript'}
861+
</span>
862+
)}
863+
</div>
864+
<CodeEditor
865+
code={userAnswer}
866+
onChange={setUserAnswer}
867+
language={currentProblem.sourceLanguage}
868+
height={120}
869+
minHeight={120}
870+
lineNumbers={false}
871+
autoFocus
872+
onSubmitShortcut={handleSubmit}
873+
className="border-zinc-700"
856874
/>
857875
<p className="text-xs text-zinc-500 mt-2">Press Cmd/Ctrl + Enter to submit</p>
858876
</div>
@@ -1152,7 +1170,7 @@ export default function DrillPage() {
11521170

11531171
const [phase, setPhase] = useState<DrillPhase>('setup');
11541172
const [config, setConfig] = useState<DrillConfig | null>(null);
1155-
const [problems, setProblems] = useState<Problem[]>([]);
1173+
const [problems, setProblems] = useState<ProblemWithLanguage[]>([]);
11561174
const [drillState, setDrillState] = useState<DrillState>({
11571175
currentIndex: 0,
11581176
answers: [],

0 commit comments

Comments
 (0)