diff --git a/src/components/ProblemWorkspace.css b/src/components/ProblemWorkspace.css index cbeb10a..fd0c8f9 100644 --- a/src/components/ProblemWorkspace.css +++ b/src/components/ProblemWorkspace.css @@ -98,6 +98,12 @@ color: var(--ik-muted); } +.problem-workspace-link--secondary { + display: inline-block; + margin-top: 0.95rem; + opacity: 0.9; +} + .problem-workspace-link:hover { color: var(--ik-cf); } @@ -110,32 +116,86 @@ margin-bottom: 0.9rem; } +.problem-workspace-editor-heading { + display: flex; + align-items: center; + gap: 0.55rem; + min-width: 0; +} + .problem-workspace-header h2, .problem-workspace-panel--results h2 { font-size: 1rem; } +.problem-workspace-language-badge { + font-size: 0.72rem; + color: var(--ik-cf); + background: rgba(74, 158, 255, 0.14); + border: 1px solid rgba(74, 158, 255, 0.25); + border-radius: 999px; + padding: 0.1rem 0.42rem; + font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, monospace; +} + +.problem-workspace-editor-problem-id { + color: var(--ik-muted); + font-size: 0.78rem; + font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, monospace; +} + .problem-workspace-actions { display: flex; + align-items: center; gap: 0.6rem; flex-wrap: wrap; } -.problem-workspace-actions button:disabled { - opacity: 0.55; +.problem-workspace-save-status { + font-size: 0.76rem; + color: var(--ik-muted); +} + +.problem-workspace-placeholder-btn { + border: 1px solid var(--ik-border); + background: var(--ik-surface); + color: var(--ik-muted); + border-radius: var(--ik-radius-sm); + font-size: 0.78rem; + font-weight: 600; + padding: 0.35rem 0.68rem; cursor: not-allowed; + opacity: 0.65; +} + +.problem-workspace-reset-btn { + border: 1px solid var(--ik-border); + background: var(--ik-surface); + color: var(--ik-text); + border-radius: var(--ik-radius-sm); + font-size: 0.78rem; + font-weight: 600; + padding: 0.35rem 0.68rem; + cursor: pointer; +} + +.problem-workspace-reset-btn:hover { + border-color: var(--ik-cf); + color: var(--ik-cf); } .problem-workspace-editor { width: 100%; - min-height: 430px; + min-height: 470px; resize: vertical; border: 1px solid var(--ik-border); border-radius: var(--ik-radius-sm); - background: #111111; + background: #101216; color: var(--ik-text); padding: 1rem; font: 0.95rem/1.5 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, monospace; + tab-size: 4; + caret-color: #8fb6ff; } .problem-workspace-editor::placeholder { diff --git a/src/components/ProblemWorkspace.jsx b/src/components/ProblemWorkspace.jsx index c875554..7b456f7 100644 --- a/src/components/ProblemWorkspace.jsx +++ b/src/components/ProblemWorkspace.jsx @@ -1,4 +1,4 @@ -import { useEffect, useMemo, useState } from 'react'; +import { useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react'; import { useLocation, useParams } from 'react-router-dom'; import { fetchProblemStatement } from '../services/cfStatement'; import { @@ -7,13 +7,16 @@ import { } from '../utils/problemWorkspace'; import './ProblemWorkspace.css'; -const DEFAULT_PLACEHOLDER = 'fun main() {\n\n}'; +const DEFAULT_KOTLIN_STARTER = `fun main() { + // solve here +}`; function loadDraft(storageKey) { try { - return localStorage.getItem(storageKey) || ''; + const saved = localStorage.getItem(storageKey); + return saved === null ? DEFAULT_KOTLIN_STARTER : saved; } catch { - return ''; + return DEFAULT_KOTLIN_STARTER; } } @@ -38,6 +41,9 @@ function ProblemWorkspace() { ); const [code, setCode] = useState(() => loadDraft(storageKey)); + const [hasEdited, setHasEdited] = useState(false); + const editorRef = useRef(null); + const pendingCaretRef = useRef(null); // Statement fetch state const [stmtStatus, setStmtStatus] = useState('idle'); // 'idle' | 'loading' | 'ok' | 'error' @@ -47,27 +53,36 @@ function ProblemWorkspace() { useEffect(() => { if (!contestId || !index) return; let cancelled = false; - setStmtStatus('loading'); - setStatement(null); - setStmtError(''); - fetchProblemStatement(contestId, index) - .then(data => { + + async function loadStatement() { + setStmtStatus('loading'); + setStatement(null); + setStmtError(''); + try { + const data = await fetchProblemStatement(contestId, index); if (!cancelled) { setStatement(data); setStmtStatus('ok'); } - }) - .catch(err => { + } catch (err) { if (!cancelled) { setStmtError(err.message || 'Failed to load statement.'); setStmtStatus('error'); } - }); + } + } + + loadStatement(); return () => { cancelled = true; }; }, [contestId, index]); useEffect(() => { - setCode(loadDraft(storageKey)); + function syncDraft() { + setCode(loadDraft(storageKey)); + setHasEdited(false); + } + + syncDraft(); }, [storageKey]); useEffect(() => { @@ -78,6 +93,52 @@ function ProblemWorkspace() { } }, [code, storageKey]); + useLayoutEffect(() => { + if (pendingCaretRef.current === null || !editorRef.current) { + return; + } + + editorRef.current.selectionStart = pendingCaretRef.current; + editorRef.current.selectionEnd = pendingCaretRef.current; + pendingCaretRef.current = null; + }, [code]); + + const handleEditorChange = event => { + setCode(event.target.value); + setHasEdited(true); + }; + + const handleEditorKeyDown = event => { + if (event.key !== 'Tab') { + return; + } + + event.preventDefault(); + const editor = event.currentTarget; + const start = editor.selectionStart; + const end = editor.selectionEnd; + const spaces = ' '; + const nextCode = `${code.slice(0, start)}${spaces}${code.slice(end)}`; + + setCode(nextCode); + setHasEdited(true); + + pendingCaretRef.current = start + spaces.length; + }; + + const handleResetDraft = () => { + const shouldReset = window.confirm( + 'Reset this local draft to the Kotlin starter code?', + ); + + if (!shouldReset) { + return; + } + + setCode(DEFAULT_KOTLIN_STARTER); + setHasEdited(true); + }; + const problemLink = `https://codeforces.com/problemset/problem/${contestId}/${index}`; const displayTitle = @@ -208,7 +269,7 @@ function ProblemWorkspace() { href={problemLink} target="_blank" rel="noopener noreferrer" - className="problem-workspace-link" + className="problem-workspace-link problem-workspace-link--secondary" > Open original Codeforces page ↗ @@ -222,22 +283,53 @@ function ProblemWorkspace() {
-

Kotlin

+
+

Kotlin

+ .kt + + {contestId} {index} + +
- - +