Skip to content

Commit 29ba361

Browse files
Problem render markdown support at backend
1 parent af0c204 commit 29ba361

File tree

5 files changed

+147
-29
lines changed

5 files changed

+147
-29
lines changed

client/package-lock.json

Lines changed: 72 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

client/package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,9 @@
4141
"react-simple-code-editor": "^0.14.1",
4242
"react-split": "^2.0.14",
4343
"react-toastify": "^11.0.5",
44+
"rehype-highlight": "^7.0.2",
4445
"rehype-katex": "^7.0.1",
46+
"rehype-sanitize": "^6.0.0",
4547
"remark-math": "^6.0.0",
4648
"tailwind-merge": "^3.3.1",
4749
"three": "^0.179.1"

client/src/pages/CompilerPage.jsx

Lines changed: 20 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,11 @@ import { Player } from '@lottiefiles/react-lottie-player';
99
import { Wand2, Check } from 'lucide-react';
1010
import Split from 'react-split';
1111
import 'katex/dist/katex.min.css';
12-
import { InlineMath } from 'react-katex';
12+
import ReactMarkdown from 'react-markdown';
13+
import remarkMath from 'remark-math';
14+
import rehypeKatex from 'rehype-katex';
15+
import rehypeHighlight from 'rehype-highlight';
16+
import rehypeSanitize from 'rehype-sanitize';
1317

1418
const defaultHelloWorld = {
1519
cpp: `#include <iostream>\nusing namespace std;\nint main() {\n cout << \"Hello, World!\";\n return 0;\n}`,
@@ -453,25 +457,6 @@ export default function CompilerPage() {
453457

454458
const renderProblemDescription = () => {
455459
if (!problem) return null;
456-
457-
const renderLinesWithLatex = (text) => {
458-
return text.split("\n").map((line, idx) => (
459-
<p key={idx} className="mb-2 leading-relaxed">
460-
{line.includes("$") ? (
461-
line.split("$").map((segment, i) =>
462-
i % 2 === 1 ? (
463-
<InlineMath math={segment} key={i} />
464-
) : (
465-
<span key={i}>{segment}</span>
466-
)
467-
)
468-
) : (
469-
line
470-
)}
471-
</p>
472-
));
473-
};
474-
475460
return (
476461
<div className="space-y-6 text-white font-sans">
477462
<h2 className="text-2xl font-extrabold mb-4 bg-gradient-to-r from-[#7286ff] to-[#fe7587] bg-clip-text text-transparent drop-shadow-lg">
@@ -484,7 +469,21 @@ export default function CompilerPage() {
484469
Description
485470
</h3>
486471
<div className="text-white text-sm leading-relaxed">
487-
{renderLinesWithLatex(problem.description)}
472+
<ReactMarkdown
473+
children={problem.description}
474+
remarkPlugins={[remarkMath]}
475+
rehypePlugins={[rehypeKatex, rehypeHighlight, rehypeSanitize]}
476+
components={{
477+
img: ({node, ...props}) => (
478+
<img {...props} style={{maxWidth: '100%', borderRadius: '8px', margin: '12px 0'}} alt={props.alt || ''} />
479+
),
480+
code: ({node, inline, className, children, ...props}) => (
481+
<code className={className} style={{background: '#232347', color: '#ffb4d0', borderRadius: '4px', padding: '2px 6px', fontSize: '0.95em'}} {...props}>
482+
{children}
483+
</code>
484+
)
485+
}}
486+
/>
488487
</div>
489488
</div>
490489

client/src/pages/CreateProblem.jsx

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,12 @@
11
import { useState } from 'react';
22
import API from '../services/api';
33
import { useNavigate } from 'react-router-dom';
4+
import ReactMarkdown from 'react-markdown';
5+
import remarkMath from 'remark-math';
6+
import rehypeKatex from 'rehype-katex';
7+
import rehypeHighlight from 'rehype-highlight';
8+
import rehypeSanitize from 'rehype-sanitize';
9+
import 'katex/dist/katex.min.css';
410

511
export default function CreateProblem() {
612
const [mode, setMode] = useState('single'); // 'single' or 'bulk'
@@ -24,6 +30,7 @@ export default function CreateProblem() {
2430
const [error, setError] = useState('');
2531
const [success, setSuccess] = useState('');
2632
const navigate = useNavigate();
33+
const [showPreview, setShowPreview] = useState(false);
2734

2835
const handleChange = (e) => {
2936
setForm({ ...form, [e.target.name]: e.target.value });
@@ -143,6 +150,37 @@ export default function CreateProblem() {
143150
Switch to {mode === 'single' ? 'Bulk Mode' : 'Single Mode'}
144151
</button>
145152
</div>
153+
{/* Preview toggle button */}
154+
{mode === 'single' && (
155+
<button
156+
type="button"
157+
className="mb-4 px-4 py-1 rounded bg-gradient-to-r from-[#7286ff] to-[#fe7587] text-white font-semibold shadow hover:brightness-110"
158+
onClick={() => setShowPreview((p) => !p)}
159+
>
160+
{showPreview ? 'Hide Preview' : 'Preview Description'}
161+
</button>
162+
)}
163+
{/* Preview area */}
164+
{showPreview && mode === 'single' && (
165+
<div className="mb-6 p-4 bg-[#18182a] rounded-lg border border-[#7286ff]/30 text-white">
166+
<h3 className="text-lg font-bold mb-2 text-[#7286ff]">Preview</h3>
167+
<ReactMarkdown
168+
children={form.description}
169+
remarkPlugins={[remarkMath]}
170+
rehypePlugins={[rehypeKatex, rehypeHighlight, rehypeSanitize]}
171+
components={{
172+
img: ({node, ...props}) => (
173+
<img {...props} style={{maxWidth: '100%', borderRadius: '8px', margin: '12px 0'}} alt={props.alt || ''} />
174+
),
175+
code: ({node, inline, className, children, ...props}) => (
176+
<code className={className} style={{background: '#232347', color: '#ffb4d0', borderRadius: '4px', padding: '2px 6px', fontSize: '0.95em'}} {...props}>
177+
{children}
178+
</code>
179+
)
180+
}}
181+
/>
182+
</div>
183+
)}
146184

147185
{error && <p className="text-red-500 mb-4">{error}</p>}
148186
{success && <p className="text-green-400 mb-4">{success}</p>}

server/controller/problemController.js

Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import redisClient from '../utils/redisClient.js'; // adjust path if needed
33

44
// Admin: Create problem
55
export const createProblem = async (req, res) => {
6-
const { title, description, difficulty, tags = [], testCases, solutionCode = {} } = req.body;
6+
let { title, description, difficulty, tags = [], testCases, solutionCode = {} } = req.body;
77

88
if (req.user.role !== 'admin') {
99
return res.status(403).json({ error: 'Access denied: Admins only' });
@@ -13,6 +13,11 @@ export const createProblem = async (req, res) => {
1313
return res.status(400).json({ error: 'At least one test case is required' });
1414
}
1515

16+
// Convert all literal '\n' in description to real newlines
17+
if (typeof description === 'string') {
18+
description = description.replace(/\\n/g, '\n');
19+
}
20+
1621
try {
1722
const problem = await Problem.create({
1823
title,
@@ -40,19 +45,21 @@ export const bulkCreateProblems = async (req, res) => {
4045
return res.status(403).json({ error: 'Access denied: Admins only' });
4146
}
4247

43-
const problems = req.body;
48+
let problems = req.body;
4449

4550
if (!Array.isArray(problems)) {
4651
return res.status(400).json({ error: 'Expected an array of problems' });
4752
}
4853

49-
try {
50-
const enrichedProblems = problems.map(p => ({
51-
...p,
52-
createdBy: req.user.id,
53-
}));
54+
// Convert all literal '\n' in description to real newlines for each problem
55+
problems = problems.map(p => ({
56+
...p,
57+
description: typeof p.description === 'string' ? p.description.replace(/\\n/g, '\n') : p.description,
58+
createdBy: req.user.id,
59+
}));
5460

55-
const result = await Problem.insertMany(enrichedProblems);
61+
try {
62+
const result = await Problem.insertMany(problems);
5663
await redisClient.del('all_problems'); // Invalidate cache
5764
res.status(201).json({ message: `${result.length} problems added.` });
5865
} catch (err) {

0 commit comments

Comments
 (0)