Implement Code Challenge component with styling and WebSocket integration

This commit is contained in:
ishikabhoyar
2025-07-22 15:26:54 +05:30
parent e61bd7cfca
commit e12bbcfc6a
4 changed files with 1017 additions and 2 deletions

View File

@@ -1,10 +1,10 @@
import VSCodeUI from "./components/VSCodeUI.jsx"
import CodeChallenge from "./components/CodeChallenge.jsx"
import "./index.css"
function App() {
return (
<div className="App">
<VSCodeUI />
<CodeChallenge />
</div>
)
}

View File

@@ -0,0 +1,282 @@
/* Code Challenge Component Styles */
.code-challenge-container {
display: flex;
flex-direction: column;
height: 100vh;
background-color: #1e1e1e;
color: #d4d4d4;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
}
.code-challenge-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 10px 20px;
background-color: #1e1e1e;
border-bottom: 1px solid #333;
}
.code-challenge-header h1 {
margin: 0;
font-size: 18px;
font-weight: 500;
}
.sign-in-btn {
background-color: #333;
color: #fff;
border: none;
padding: 5px 10px;
border-radius: 3px;
cursor: pointer;
}
.sign-in-btn:hover {
background-color: #444;
}
.code-challenge-problem-nav {
padding: 10px 20px;
border-bottom: 1px solid #333;
}
.problem-number {
margin: 0;
font-size: 16px;
font-weight: 500;
}
.code-challenge-main {
display: flex;
flex-direction: row;
height: calc(70vh - 120px);
overflow: hidden;
}
.left-panel {
display: flex;
flex-direction: column;
width: 50%;
border-right: 1px solid #333;
}
.problem-tabs {
display: flex;
width: 100%;
height: 40px;
border-bottom: 1px solid #333;
}
.problem-tabs button {
min-width: 120px;
padding: 0 20px;
background-color: #252526;
color: #d4d4d4;
border: none;
text-align: center;
cursor: pointer;
border-right: 1px solid #333;
font-size: 14px;
}
.problem-tabs button.tab-active {
background-color: #1e1e1e;
border-left: 2px solid #0078d4;
color: #ffffff;
}
.problem-tabs button:hover:not(.tab-active) {
background-color: #252526;
}
.problem-content {
flex: 1;
overflow-y: auto;
padding: 20px;
border-right: 1px solid #333;
}
.problem-container h1 {
margin-top: 0;
margin-bottom: 15px;
font-size: 24px;
}
.problem-description {
margin-bottom: 20px;
line-height: 1.5;
}
.problem-description code {
background-color: #2d2d2d;
padding: 2px 4px;
border-radius: 3px;
font-family: 'Consolas', 'Courier New', monospace;
}
/* Removed problem examples styles */
.editor-section {
flex: 1;
display: flex;
flex-direction: column;
border-left: 1px solid #333;
}
.editor-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 10px;
background-color: #252526;
border-bottom: 1px solid #333;
}
.editor-controls {
display: flex;
align-items: center;
}
.language-selector {
background-color: #2d2d2d;
color: #d4d4d4;
border: 1px solid #3c3c3c;
padding: 5px 10px;
border-radius: 3px;
margin-right: 10px;
}
.auto-btn {
background-color: #252526;
color: #d4d4d4;
border: 1px solid #3c3c3c;
padding: 5px 10px;
border-radius: 3px;
cursor: pointer;
}
.auto-selected {
background-color: #0e639c;
border-color: #0e639c;
color: white;
}
.editor-actions {
display: flex;
align-items: center;
}
.run-btn {
display: flex;
align-items: center;
background-color: #252526;
color: #d4d4d4;
border: 1px solid #3c3c3c;
padding: 5px 10px;
border-radius: 3px;
margin-right: 10px;
cursor: pointer;
}
.submit-btn {
display: flex;
align-items: center;
background-color: #0e8a16;
color: white;
border: none;
padding: 5px 10px;
border-radius: 3px;
cursor: pointer;
}
.run-btn svg, .submit-btn svg {
margin-right: 5px;
}
.run-btn:hover {
background-color: #2d2d2d;
}
.submit-btn:hover {
background-color: #0ca01c;
}
.editor-container {
flex: 1;
overflow: hidden;
}
.terminal-section {
height: 30vh;
background-color: #1e1e1e;
border-top: 1px solid #333;
}
.terminal-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 5px 10px;
background-color: #252526;
border-bottom: 1px solid #333;
}
.terminal-controls {
display: flex;
align-items: center;
}
.terminal-btn {
background: none;
border: none;
color: #d4d4d4;
font-size: 12px;
cursor: pointer;
margin-left: 5px;
}
.terminal-content {
padding: 10px;
font-family: 'Consolas', 'Courier New', monospace;
height: calc(30vh - 40px);
overflow-y: auto;
}
.terminal-line {
margin-bottom: 5px;
word-wrap: break-word;
white-space: pre-wrap;
}
.terminal-line.system {
color: #569cd6;
}
.terminal-line.output {
color: #d4d4d4;
}
.terminal-line.error {
color: #f48771;
}
.terminal-prompt {
display: flex;
align-items: center;
margin-top: 5px;
}
.prompt-symbol {
margin-right: 5px;
color: #569cd6;
}
.terminal-input {
flex: 1;
background: none;
border: none;
color: #d4d4d4;
font-family: 'Consolas', 'Courier New', monospace;
outline: none;
}

View File

@@ -0,0 +1,451 @@
import React, { useState, useEffect, useRef } from 'react';
import Editor from "@monaco-editor/react";
import { Play, Send } from 'lucide-react';
const CodeChallenge = () => {
const [activeQuestion, setActiveQuestion] = useState("Q.1");
const [language, setLanguage] = useState("JavaScript");
const [code, setCode] = useState("");
const [isRunning, setIsRunning] = useState(false);
const [terminalOutput, setTerminalOutput] = useState([]);
const [autoSelected, setAutoSelected] = useState(true);
const [activeSocket, setActiveSocket] = useState(null);
const [submissionId, setSubmissionId] = useState(null);
const socketRef = useRef(null);
// Example problem data
const problems = {
"Q.1": {
id: "two-sum",
title: "Two Sum",
description: "Given an array of integers nums and an integer target, return indices of the two numbers such that they add up to target.",
constraints: "You may assume that each input would have exactly one solution, and you may not use the same element twice.",
examples: [
{
input: "nums = [2,7,11,15], target = 9",
output: "[0,1]",
explanation: "Because nums[0] + nums[1] == 9, we return [0, 1]."
},
{
input: "nums = [3,2,4], target = 6",
output: "[1,2]"
},
{
input: "nums = [3,3], target = 6",
output: "[0,1]"
}
],
starterCode: `/**
* @param {number[]} nums
* @param {number} target
* @return {number[]}
*/
var twoSum = function(nums, target) {
// Write your solution here
};`
},
"Q.2": {
id: "palindrome-number",
title: "Palindrome Number",
description: "Given an integer x, return true if x is a palindrome, and false otherwise.",
examples: [
{
input: "x = 121",
output: "true"
},
{
input: "x = -121",
output: "false",
explanation: "From left to right, it reads -121. From right to left, it reads 121-. Therefore it is not a palindrome."
}
],
starterCode: `/**
* @param {number} x
* @return {boolean}
*/
var isPalindrome = function(x) {
// Write your solution here
};`
},
"Q.3": {
id: "valid-parentheses",
title: "Valid Parentheses",
description: "Given a string s containing just the characters '(', ')', '{', '}', '[' and ']', determine if the input string is valid.",
constraints: "An input string is valid if: Open brackets must be closed by the same type of brackets. Open brackets must be closed in the correct order.",
examples: [
{
input: 's = "()"',
output: "true"
},
{
input: 's = "()[]{}"',
output: "true"
},
{
input: 's = "(]"',
output: "false"
}
],
starterCode: `/**
* @param {string} s
* @return {boolean}
*/
var isValid = function(s) {
// Write your solution here
};`
}
};
// Set initial code based on active problem
useEffect(() => {
if (problems[activeQuestion]) {
setCode(problems[activeQuestion].starterCode);
}
}, [activeQuestion]);
// Cleanup WebSocket connection on unmount
useEffect(() => {
return () => {
if (socketRef.current) {
socketRef.current.close();
}
};
}, []);
// Connect to WebSocket
const connectToWebSocket = (id) => {
// Close existing connection if any
if (socketRef.current && socketRef.current.readyState === WebSocket.OPEN) {
socketRef.current.close();
}
const wsUrl = `ws://localhost:8080/api/ws/terminal/${id}`;
const socket = new WebSocket(wsUrl);
socket.onopen = () => {
console.log('WebSocket connection established');
setActiveSocket(socket);
};
socket.onmessage = (event) => {
try {
const message = JSON.parse(event.data);
switch (message.type) {
case 'output':
setTerminalOutput(prev => [
...prev,
{
type: message.content.isError ? 'error' : 'output',
content: message.content.text
}
]);
break;
case 'status':
setTerminalOutput(prev => [
...prev,
{ type: 'system', content: `Status: ${message.content.status}` }
]);
if (message.content.status === 'completed' || message.content.status === 'failed') {
setIsRunning(false);
}
break;
case 'error':
setTerminalOutput(prev => [
...prev,
{ type: 'error', content: message.content.message }
]);
setIsRunning(false);
break;
case 'system':
setTerminalOutput(prev => [
...prev,
{ type: 'system', content: message.content }
]);
break;
default:
console.log('Unknown message type:', message);
}
} catch (error) {
console.error('Error parsing WebSocket message:', error);
}
};
socket.onerror = (error) => {
console.error('WebSocket error:', error);
setTerminalOutput(prev => [
...prev,
{ type: 'error', content: 'WebSocket connection error' }
]);
setIsRunning(false);
};
socket.onclose = () => {
console.log('WebSocket connection closed');
setActiveSocket(null);
};
socketRef.current = socket;
return socket;
};
// Handle code execution
const runCode = async () => {
setIsRunning(true);
setTerminalOutput([
{ type: 'system', content: `Running ${problems[activeQuestion].id}...` }
]);
try {
// Submit code to the backend
const response = await fetch('http://localhost:8080/api/submit', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
code: code,
language: language.toLowerCase(),
input: '', // Add input if needed
}),
});
if (!response.ok) {
throw new Error(`Error: ${response.statusText}`);
}
const data = await response.json();
setSubmissionId(data.id);
// Connect to WebSocket for real-time updates
connectToWebSocket(data.id);
} catch (error) {
console.error('Error submitting code:', error);
setTerminalOutput(prev => [
...prev,
{ type: 'error', content: `Error: ${error.message}` }
]);
setIsRunning(false);
}
};
// Handle code submission
const submitCode = async () => {
setIsRunning(true);
setTerminalOutput([
{ type: 'system', content: `Submitting solution for ${problems[activeQuestion].id}...` }
]);
try {
// Submit code to the backend
const response = await fetch('http://localhost:8080/api/submit', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
code: code,
language: language.toLowerCase(),
input: '',
problemId: problems[activeQuestion].id
}),
});
if (!response.ok) {
throw new Error(`Error: ${response.statusText}`);
}
const data = await response.json();
setSubmissionId(data.id);
// Connect to WebSocket for real-time updates
connectToWebSocket(data.id);
} catch (error) {
console.error('Error submitting solution:', error);
setTerminalOutput(prev => [
...prev,
{ type: 'error', content: `Error: ${error.message}` }
]);
setIsRunning(false);
}
};
// Render the current problem
const renderProblem = () => {
const problem = problems[activeQuestion];
if (!problem) return null;
return (
<div className="problem-container">
<h1>{problem.title}</h1>
<div className="problem-description">
<p>{problem.description}</p>
{problem.constraints && <p>{problem.constraints}</p>}
</div>
{/* Test cases section removed */}
</div>
);
};
return (
<div className="code-challenge-container">
<header className="code-challenge-header">
<h1>OnScreen Test</h1>
<button className="sign-in-btn">Sign In</button>
</header>
<div className="code-challenge-problem-nav">
<h3 className="problem-number">1. {problems["Q.1"].title}</h3>
</div>
<div className="code-challenge-main">
<div className="problem-tabs">
<button
className={activeQuestion === "Q.1" ? "tab-active" : ""}
onClick={() => setActiveQuestion("Q.1")}
>
Q.1
</button>
<button
className={activeQuestion === "Q.2" ? "tab-active" : ""}
onClick={() => setActiveQuestion("Q.2")}
>
Q.2
</button>
<button
className={activeQuestion === "Q.3" ? "tab-active" : ""}
onClick={() => setActiveQuestion("Q.3")}
>
Q.3
</button>
</div>
<div className="problem-content">
{renderProblem()}
</div>
<div className="editor-section">
<div className="editor-header">
<div className="editor-controls">
<select
value={language}
onChange={(e) => setLanguage(e.target.value)}
className="language-selector"
>
<option value="JavaScript">JavaScript</option>
<option value="Python">Python</option>
<option value="Java">Java</option>
<option value="C++">C++</option>
</select>
<button
className={`auto-btn ${autoSelected ? 'auto-selected' : ''}`}
onClick={() => setAutoSelected(!autoSelected)}
>
Auto
</button>
</div>
<div className="editor-actions">
<button
className="run-btn"
onClick={runCode}
disabled={isRunning}
>
<Play size={16} /> Run
</button>
<button
className="submit-btn"
onClick={submitCode}
disabled={isRunning}
>
<Send size={16} /> Submit
</button>
</div>
</div>
<div className="editor-container">
<Editor
height="100%"
defaultLanguage="javascript"
language={language.toLowerCase()}
value={code}
onChange={(value) => setCode(value)}
theme="vs-dark"
options={{
fontSize: 14,
minimap: { enabled: false },
scrollBeyondLastLine: false,
automaticLayout: true,
}}
/>
</div>
</div>
</div>
<div className="terminal-section">
<div className="terminal-header">
<span>Terminal</span>
<div className="terminal-controls">
<button className="terminal-btn"></button>
<button className="terminal-btn"></button>
<button className="terminal-btn"></button>
</div>
</div>
<div className="terminal-content">
{terminalOutput.map((line, index) => (
<div
key={index}
className={`terminal-line ${line.type}`}
>
{line.content}
</div>
))}
<div className="terminal-prompt">
<span className="prompt-symbol">$</span>
<input
type="text"
className="terminal-input"
placeholder="Type here..."
disabled={!isRunning}
onKeyPress={(e) => {
if (e.key === 'Enter' && activeSocket) {
const input = e.target.value;
// Send input to WebSocket
activeSocket.send(JSON.stringify({
type: 'input',
content: { text: input }
}));
// Add input to terminal output
setTerminalOutput(prev => [
...prev,
{ type: 'system', content: `$ ${input}` }
]);
// Clear the input field
e.target.value = '';
}
}}
/>
</div>
</div>
</div>
</div>
);
};
export default CodeChallenge;

View File

@@ -1025,4 +1025,286 @@ body {
.panel-close-btn:hover {
opacity: 1;
}
/* Code Challenge Component Styles */
.code-challenge-container {
display: flex;
flex-direction: column;
height: 100vh;
background-color: var(--vscode-background);
color: var(--vscode-foreground);
}
.code-challenge-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 8px 16px;
background-color: var(--vscode-background);
border-bottom: 1px solid rgba(128, 128, 128, 0.35);
}
.code-challenge-header h1 {
margin: 0;
font-size: 18px;
font-weight: 400;
}
.sign-in-btn {
background-color: transparent;
color: var(--vscode-foreground);
border: 1px solid rgba(128, 128, 128, 0.5);
padding: 4px 12px;
border-radius: 4px;
font-size: 14px;
cursor: pointer;
}
.sign-in-btn:hover {
background-color: rgba(255, 255, 255, 0.05);
}
.code-challenge-problem-nav {
padding: 8px 16px;
border-bottom: 1px solid rgba(128, 128, 128, 0.35);
}
.problem-number {
margin: 0;
font-size: 16px;
font-weight: 400;
}
.code-challenge-main {
display: flex;
height: 60vh;
border-bottom: 1px solid rgba(128, 128, 128, 0.35);
}
.problem-tabs {
display: flex;
flex-direction: column;
width: 130px;
border-right: 1px solid rgba(128, 128, 128, 0.35);
}
.problem-tabs button {
padding: 16px;
background-color: transparent;
color: var(--vscode-foreground);
border: none;
text-align: left;
cursor: pointer;
border-bottom: 1px solid rgba(128, 128, 128, 0.2);
}
.problem-tabs button.tab-active {
background-color: var(--vscode-background);
font-weight: 500;
border-left: 2px solid #007acc;
}
.problem-tabs button:hover:not(.tab-active) {
background-color: rgba(255, 255, 255, 0.05);
}
.problem-content {
flex: 1;
overflow-y: auto;
padding: 16px;
border-right: 1px solid rgba(128, 128, 128, 0.35);
width: 50%;
}
.problem-container h1 {
margin-top: 0;
font-size: 22px;
margin-bottom: 16px;
}
.problem-description {
margin-bottom: 20px;
font-size: 14px;
line-height: 1.5;
}
.problem-examples h2 {
font-size: 16px;
margin-top: 24px;
margin-bottom: 12px;
}
.example-box {
background-color: rgba(0, 0, 0, 0.3);
padding: 12px;
border-radius: 6px;
margin-bottom: 12px;
font-family: 'Consolas', 'Monaco', monospace;
font-size: 14px;
line-height: 1.5;
}
.editor-section {
width: 50%;
display: flex;
flex-direction: column;
}
.editor-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 8px 12px;
background-color: #252526;
border-bottom: 1px solid rgba(128, 128, 128, 0.35);
}
.editor-controls {
display: flex;
align-items: center;
}
.language-selector {
background-color: #3c3c3c;
color: #d4d4d4;
border: 1px solid #3c3c3c;
border-radius: 4px;
padding: 4px 8px;
font-size: 13px;
margin-right: 8px;
}
.auto-btn {
background-color: #3c3c3c;
color: #d4d4d4;
border: none;
border-radius: 4px;
padding: 4px 12px;
font-size: 13px;
cursor: pointer;
}
.auto-selected {
background-color: #4d4d4d;
}
.editor-actions {
display: flex;
gap: 8px;
}
.run-btn {
display: flex;
align-items: center;
gap: 4px;
background-color: #3c3c3c;
color: #d4d4d4;
border: none;
border-radius: 4px;
padding: 4px 12px;
font-size: 13px;
cursor: pointer;
}
.submit-btn {
display: flex;
align-items: center;
gap: 4px;
background-color: #0e639c;
color: #ffffff;
border: none;
border-radius: 4px;
padding: 4px 12px;
font-size: 13px;
cursor: pointer;
}
.run-btn:hover {
background-color: #4d4d4d;
}
.submit-btn:hover {
background-color: #1177bb;
}
.editor-container {
flex: 1;
}
.terminal-section {
flex: 1;
display: flex;
flex-direction: column;
background-color: var(--vscode-panel-background);
}
.terminal-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 4px 12px;
background-color: #252526;
font-size: 13px;
}
.terminal-controls {
display: flex;
gap: 4px;
}
.terminal-btn {
background-color: transparent;
color: #d4d4d4;
border: none;
cursor: pointer;
font-size: 12px;
width: 20px;
height: 20px;
display: flex;
align-items: center;
justify-content: center;
}
.terminal-content {
flex: 1;
padding: 8px 12px;
font-family: 'Consolas', 'Monaco', monospace;
font-size: 14px;
overflow-y: auto;
white-space: pre-wrap;
}
.terminal-line {
margin-bottom: 4px;
line-height: 1.4;
}
.terminal-line.system {
color: #569cd6;
}
.terminal-line.error {
color: #f48771;
}
.terminal-prompt {
display: flex;
align-items: center;
margin-top: 8px;
}
.prompt-symbol {
color: #569cd6;
margin-right: 8px;
}
.terminal-input {
background-color: transparent;
color: #d4d4d4;
border: none;
outline: none;
flex: 1;
font-family: 'Consolas', 'Monaco', monospace;
font-size: 14px;
}