Skip to content
Merged
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
68 changes: 64 additions & 4 deletions src/components/ProblemWorkspace.css
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand All @@ -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 {
Expand Down
130 changes: 111 additions & 19 deletions src/components/ProblemWorkspace.jsx
Original file line number Diff line number Diff line change
@@ -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 {
Expand All @@ -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;
}
}

Expand All @@ -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'
Expand All @@ -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(() => {
Expand All @@ -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 =
Expand Down Expand Up @@ -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 ↗
</a>
Expand All @@ -222,22 +283,53 @@ function ProblemWorkspace() {
<section className="problem-workspace-main">
<div className="problem-workspace-panel problem-workspace-panel--editor">
<div className="problem-workspace-header">
<h2>Kotlin</h2>
<div className="problem-workspace-editor-heading">
<h2>Kotlin</h2>
<span className="problem-workspace-language-badge">.kt</span>
<span
className="problem-workspace-editor-problem-id"
aria-label={`Problem ${contestId} ${index}`}
>
{contestId} {index}
</span>
</div>
<div className="problem-workspace-actions">
<button type="button" disabled>
{hasEdited && (
<span className="problem-workspace-save-status">
Saved locally
</span>
)}
<button
type="button"
className="problem-workspace-placeholder-btn"
disabled
>
Run Samples
</button>
<button type="button" disabled>
<button
type="button"
className="problem-workspace-placeholder-btn"
disabled
>
Prepare Submit
</button>
<button
type="button"
className="problem-workspace-reset-btn"
onClick={handleResetDraft}
>
Reset Draft
</button>
</div>
</div>

<textarea
ref={editorRef}
className="problem-workspace-editor"
value={code}
onChange={event => setCode(event.target.value)}
placeholder={DEFAULT_PLACEHOLDER}
onChange={handleEditorChange}
onKeyDown={handleEditorKeyDown}
placeholder={DEFAULT_KOTLIN_STARTER}
spellCheck="false"
aria-label="Kotlin code editor"
/>
Expand Down
Loading