diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index 23e8e0f..022da6d 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -3,6 +3,7 @@ import { AuthProvider, useAuth } from './context/AuthContext' import ChallengeListPage from './pages/ChallengeListPage' import ChallengePage from './pages/ChallengePage' import CreateChallengePage from './pages/CreateChallengePage' +import EditChallengePage from './pages/EditChallengePage' import AboutPage from './pages/AboutPage' import LoginPage from './pages/LoginPage' import RegisterPage from './pages/RegisterPage' @@ -20,6 +21,7 @@ function AppRoutes() { } /> } /> } /> + } /> } /> } /> diff --git a/frontend/src/pages/ChallengeListPage.jsx b/frontend/src/pages/ChallengeListPage.jsx index 302913d..4b1bba9 100644 --- a/frontend/src/pages/ChallengeListPage.jsx +++ b/frontend/src/pages/ChallengeListPage.jsx @@ -31,6 +31,7 @@ export default function ChallengeListPage() { return ( <> +

@@ -68,6 +69,7 @@ export default function ChallengeListPage() { Title Type Difficulty + Author @@ -93,6 +95,9 @@ export default function ChallengeListPage() { {c.difficulty} + + {c.createdBy ?? '—'} + ))} diff --git a/frontend/src/pages/ChallengePage.jsx b/frontend/src/pages/ChallengePage.jsx index 4e4d825..3b6706d 100644 --- a/frontend/src/pages/ChallengePage.jsx +++ b/frontend/src/pages/ChallengePage.jsx @@ -1,8 +1,9 @@ import { useEffect, useRef, useState } from 'react' -import { useParams, Link } from 'react-router-dom' +import { useParams, Link, useNavigate } from 'react-router-dom' import Editor from '@monaco-editor/react' import api from '../api/axios' import NavBar from '../components/NavBar' +import { useAuth } from '../context/AuthContext' function sortKeys(val) { if (Array.isArray(val)) return val.map(sortKeys) @@ -59,6 +60,8 @@ function compareResult(challengeType, actual, expected) { export default function ChallengePage() { const { id } = useParams() + const navigate = useNavigate() + const { user } = useAuth() const [challenge, setChallenge] = useState(null) const [error, setError] = useState(null) const [activeSchema, setActiveSchema] = useState(0) @@ -166,11 +169,52 @@ export default function ChallengePage() {

-

- {challenge.title} -

+
+

+ {challenge.title} +

+ {(user?.username === challenge.createdBy || user?.role === 'ADMIN') && ( +
+ + Edit + + +
+ )} +
+ {challenge.createdBy && ( +

by {challenge.createdBy}

+ )} -

+

{challenge.description}

diff --git a/frontend/src/pages/EditChallengePage.jsx b/frontend/src/pages/EditChallengePage.jsx new file mode 100644 index 0000000..1a50c81 --- /dev/null +++ b/frontend/src/pages/EditChallengePage.jsx @@ -0,0 +1,307 @@ +import { useEffect, useState } from 'react' +import { useNavigate, useParams, Link } from 'react-router-dom' +import api from '../api/axios' + +const inputStyle = { + width: '100%', + background: '#2d3748', + border: '1px solid #4a5568', + borderRadius: 6, + color: '#f7fafc', + padding: '8px 12px', + fontSize: 14, + boxSizing: 'border-box', +} + +const monoStyle = { ...inputStyle, fontFamily: 'monospace', fontSize: 13, minHeight: 120, resize: 'vertical' } + +const labelStyle = { display: 'block', color: '#a0aec0', fontSize: 13, marginBottom: 6 } + +const fieldStyle = { marginBottom: 20 } + +const sectionStyle = { + border: '1px solid #2d3748', + borderRadius: 8, + padding: '16px 16px 8px', + marginBottom: 12, + background: '#1a202c', +} + +const addBtnStyle = { + background: 'none', + border: '1px dashed #4a5568', + color: '#a0aec0', + borderRadius: 6, + padding: '6px 16px', + fontSize: 13, + cursor: 'pointer', + width: '100%', + marginBottom: 20, +} + +const removeBtnStyle = { + background: 'none', + border: 'none', + color: '#fc8181', + fontSize: 12, + cursor: 'pointer', + padding: '0 4px', +} + +function emptySchema() { + return { label: '', format: 'JSON_SCHEMA', content: '' } +} + +function emptyTestCase() { + return { description: '', inputJson: '', expectedJson: '', hidden: false } +} + +export default function EditChallengePage() { + const { id } = useParams() + const navigate = useNavigate() + const [loading, setLoading] = useState(true) + const [form, setForm] = useState({ + title: '', + description: '', + difficulty: 'EASY', + type: 'SCHEMA_MATCHING', + starterCode: '', + harnessCode: '', + }) + const [schemas, setSchemas] = useState([emptySchema()]) + const [testCases, setTestCases] = useState([emptyTestCase()]) + const [error, setError] = useState(null) + const [submitting, setSubmitting] = useState(false) + + useEffect(() => { + api.get(`/challenges/${id}`).then(res => { + const c = res.data + setForm({ + title: c.title ?? '', + description: c.description ?? '', + difficulty: c.difficulty ?? 'EASY', + type: c.type ?? 'SCHEMA_MATCHING', + starterCode: c.starterCode ?? '', + harnessCode: c.harnessCode ?? '', + }) + setSchemas(c.schemas?.length ? c.schemas.map(s => ({ + label: s.label ?? '', + format: s.format ?? 'JSON_SCHEMA', + content: s.content ?? '', + })) : [emptySchema()]) + setTestCases(c.testCases?.length ? c.testCases.map(t => ({ + description: t.description ?? '', + inputJson: t.inputJson ?? '', + expectedJson: t.expectedJson ?? '', + hidden: t.hidden ?? false, + })) : [emptyTestCase()]) + setLoading(false) + }).catch(() => { + setError('Failed to load challenge.') + setLoading(false) + }) + }, [id]) + + function setField(field) { + return e => setForm(f => ({ ...f, [field]: e.target.value })) + } + + function updateSchema(i, field, value) { + setSchemas(prev => prev.map((s, idx) => idx === i ? { ...s, [field]: value } : s)) + } + + function updateTestCase(i, field, value) { + setTestCases(prev => prev.map((t, idx) => idx === i ? { ...t, [field]: value } : t)) + } + + function addSchema() { setSchemas(prev => [...prev, emptySchema()]) } + function removeSchema(i) { setSchemas(prev => prev.filter((_, idx) => idx !== i)) } + + function addTestCase() { setTestCases(prev => [...prev, emptyTestCase()]) } + function removeTestCase(i) { setTestCases(prev => prev.filter((_, idx) => idx !== i)) } + + async function handleSubmit(e) { + e.preventDefault() + if (!form.title.trim()) { setError('Title is required.'); return } + setError(null) + setSubmitting(true) + try { + const payload = { + ...form, + schemas: schemas.filter(s => s.label.trim() || s.content.trim()), + testCases: testCases.filter(t => t.inputJson.trim() || t.expectedJson.trim()), + } + await api.put(`/challenges/${id}`, payload) + navigate(`/challenges/${id}`) + } catch { + setError('Failed to save changes. Please try again.') + setSubmitting(false) + } + } + + if (loading) return
Loading…
+ + return ( +
+
+

Edit Challenge

+ Cancel +
+ + {error &&

{error}

} + +
+ {/* ── Basic fields ── */} +
+ + +
+ +
+ +