diff --git a/package-lock.json b/package-lock.json index ec58d7d..be4e30c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,6 +8,7 @@ "name": "frontend", "version": "0.1.0", "dependencies": { + "@monaco-editor/react": "^4.7.0", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "next": "16.0.1", @@ -986,6 +987,29 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@monaco-editor/loader": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/@monaco-editor/loader/-/loader-1.7.0.tgz", + "integrity": "sha512-gIwR1HrJrrx+vfyOhYmCZ0/JcWqG5kbfG7+d3f/C1LXk2EvzAbHSg3MQ5lO2sMlo9izoAZ04shohfKLVT6crVA==", + "license": "MIT", + "dependencies": { + "state-local": "^1.0.6" + } + }, + "node_modules/@monaco-editor/react": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/@monaco-editor/react/-/react-4.7.0.tgz", + "integrity": "sha512-cyzXQCtO47ydzxpQtCGSQGOC8Gk3ZUeBXFAxD+CWXYFo5OqZyZUonFl0DwUlTyAfRHntBfw2p3w4s9R6oe1eCA==", + "license": "MIT", + "dependencies": { + "@monaco-editor/loader": "^1.5.0" + }, + "peerDependencies": { + "monaco-editor": ">= 0.25.0 < 1", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, "node_modules/@napi-rs/wasm-runtime": { "version": "0.2.12", "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-0.2.12.tgz", @@ -1540,6 +1564,14 @@ "@types/react": "^19.2.0" } }, + "node_modules/@types/trusted-types": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", + "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==", + "license": "MIT", + "optional": true, + "peer": true + }, "node_modules/@typescript-eslint/eslint-plugin": { "version": "8.46.2", "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.46.2.tgz", @@ -2806,6 +2838,16 @@ "node": ">=0.10.0" } }, + "node_modules/dompurify": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.2.7.tgz", + "integrity": "sha512-WhL/YuveyGXJaerVlMYGWhvQswa7myDG17P7Vu65EWC05o8vfeNbvNf4d/BOvH99+ZW+LlQsc1GDKMa1vNK6dw==", + "license": "(MPL-2.0 OR Apache-2.0)", + "peer": true, + "optionalDependencies": { + "@types/trusted-types": "^2.0.7" + } + }, "node_modules/dunder-proto": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", @@ -4879,6 +4921,19 @@ "@jridgewell/sourcemap-codec": "^1.5.5" } }, + "node_modules/marked": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/marked/-/marked-14.0.0.tgz", + "integrity": "sha512-uIj4+faQ+MgHgwUW1l2PsPglZLOLOT1uErt06dAPtx2kjteLAkbsd/0FiYg/MGS+i7ZKLb7w2WClxHkzOOuryQ==", + "license": "MIT", + "peer": true, + "bin": { + "marked": "bin/marked.js" + }, + "engines": { + "node": ">= 18" + } + }, "node_modules/math-intrinsics": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", @@ -4936,6 +4991,17 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/monaco-editor": { + "version": "0.55.1", + "resolved": "https://registry.npmjs.org/monaco-editor/-/monaco-editor-0.55.1.tgz", + "integrity": "sha512-jz4x+TJNFHwHtwuV9vA9rMujcZRb0CEilTEwG2rRSpe/A7Jdkuj8xPKttCgOh+v/lkHy7HsZ64oj+q3xoAFl9A==", + "license": "MIT", + "peer": true, + "dependencies": { + "dompurify": "3.2.7", + "marked": "14.0.0" + } + }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", @@ -5852,6 +5918,12 @@ "dev": true, "license": "MIT" }, + "node_modules/state-local": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/state-local/-/state-local-1.0.7.tgz", + "integrity": "sha512-HTEHMNieakEnoe33shBYcZ7NX83ACUjCu8c40iOGEZsngj9zRnkqS9j1pqQPXwobB0ZcVTk27REb7COQ0UR59w==", + "license": "MIT" + }, "node_modules/stop-iteration-iterator": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.1.0.tgz", diff --git a/package.json b/package.json index 5563a57..6caed4f 100644 --- a/package.json +++ b/package.json @@ -9,6 +9,7 @@ "lint": "eslint" }, "dependencies": { + "@monaco-editor/react": "^4.7.0", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "next": "16.0.1", diff --git a/src/app/problems/[slug]/page.tsx b/src/app/problems/[slug]/page.tsx new file mode 100644 index 0000000..81d1f9d --- /dev/null +++ b/src/app/problems/[slug]/page.tsx @@ -0,0 +1,185 @@ +"use client"; + +import { useState, useEffect } from "react"; +import { useParams } from "next/navigation"; +import Editor from "@monaco-editor/react"; +import Navbar from "../../components/Navbar"; +import Sidebar from "../../components/Sidebar"; +import { + fetchProblemBySlug, + submitSolution, + ProblemDetail, + SubmissionResult, +} from "@/lib/api"; + +export default function ProblemSolvePage() { + const { slug } = useParams(); + const [problem, setProblem] = useState(null); + const [code, setCode] = useState( + "# Write your Python solution here\n\ndef solve():\n pass" + ); + const [output, setOutput] = useState(null); + const [isSidebarOpen, setIsSidebarOpen] = useState(false); // Closed by default for more coding space + const [isSubmitting, setIsSubmitting] = useState(false); + + useEffect(() => { + if (slug) { + fetchProblemBySlug(slug as string) + .then(setProblem) + .catch(console.error); + } + }, [slug]); + + const handleSubmit = async () => { + if (!problem) return; + + setIsSubmitting(true); + // TODO: Get real token from AuthContext + const token = localStorage.getItem("accessToken") || ""; + + try { + const result = await submitSolution(problem._id, code, 71, token); // 71 is Python + setOutput(result); + } catch (e) { + console.error(e); + } finally { + setIsSubmitting(false); + } + }; + + if (!problem) return
Loading Problem...
; + + return ( +
+ setIsSidebarOpen(false)} /> + +
+ + +
+ {/* Left Panel: Problem Description */} +
+
+

+ {problem.title} +

+
+ + {problem.difficulty} + + {problem.tags?.map((tag) => ( + + {tag} + + ))} +
+
+ +
+

{problem.description}

+
+ + {/* Sample Test Cases Display */} + {problem.sampleTestCases?.map((tc, idx) => ( +
+

+ Example {idx + 1} +

+
+
+ Input: {tc.input} +
+
+ Output: {tc.output} +
+ {tc.explanation && ( +
+ Explanation:{" "} + {tc.explanation} +
+ )} +
+
+ ))} +
+ + {/* Right Panel: Code Editor */} +
+
+ Python 3.8 + +
+ +
+ setCode(value || "")} + options={{ + minimap: { enabled: false }, + fontSize: 14, + scrollBeyondLastLine: false, + }} + /> +
+ + {/* Output Console */} + {output && ( +
+

+ Result +

+ {output.status === "Accepted" ? ( +
+ ✓ Accepted{" "} + {typeof output.runtime === "number" && ( + + Runtime: {output.runtime}s + + )} +
+ ) : ( +
+ ✗ {output.status} + {output.errorMessage && ( +
+ {output.errorMessage} +
+ )} +
+ )} +
+ )} +
+
+
+
+ ); +} diff --git a/src/app/problems/page.tsx b/src/app/problems/page.tsx index 993e0dc..90fbd11 100644 --- a/src/app/problems/page.tsx +++ b/src/app/problems/page.tsx @@ -1,6 +1,7 @@ "use client"; import { useState, useMemo, useEffect } from "react"; +import { useRouter } from "next/navigation"; // Use Next.js router import Sidebar from "../components/Sidebar"; import Navbar from "../components/Navbar"; import ProblemsTable from "../components/ProblemsTable"; @@ -102,6 +103,8 @@ const SAMPLE_PROBLEMS: Problem[] = [ ]; export default function ProblemsPage() { + const router = useRouter(); + const [problems, setProblems] = useState([]); const [isLoading, setIsLoading] = useState(true); const [filters, setFilters] = useState({ @@ -339,7 +342,13 @@ export default function ProblemsPage() { ) : ( console.log("Navigate to problem:", id)} + // navigate to dynamic slug page instead of just logging id + onProblemClick={(id) => { + const problem = problems.find((p) => p.id === id); + if (problem?.slug) { + router.push(`/problems/${problem.slug}`); + } + }} /> )} diff --git a/src/lib/api.ts b/src/lib/api.ts index c989622..755338d 100644 --- a/src/lib/api.ts +++ b/src/lib/api.ts @@ -1,7 +1,44 @@ import { Problem } from "@/types"; +// Update to match your backend port (5001) const API_BASE_URL = - process.env.NEXT_PUBLIC_API_URL || "http://localhost:8000/api"; + process.env.NEXT_PUBLIC_API_URL || "http://localhost:5001/api"; + +// Shape of the problem list response from the backend +interface ProblemApiResponse { + _id: string; + slug: string; + title: string; + difficulty: Problem["difficulty"]; + tags?: string[]; +} + +// Shape of a sample test case in the problem detail +export interface SampleTestCase { + input: string; + output: string; + explanation?: string; +} + +// Full problem detail returned by GET /problems/:slug +export interface ProblemDetail { + _id: string; + slug: string; + title: string; + difficulty: Problem["difficulty"]; + tags?: string[]; + description?: string; + sampleTestCases?: SampleTestCase[]; +} + +// Result returned by /submissions +export interface SubmissionResult { + status: string; + runtime?: number; + errorMessage?: string; + // allow extra fields without using `any` + [key: string]: unknown; +} export async function fetchProblems(): Promise { try { @@ -9,6 +46,7 @@ export async function fetchProblems(): Promise { method: "GET", headers: { "Content-Type": "application/json", + // Add Authorization header if you implement protected problem lists later }, }); @@ -16,124 +54,59 @@ export async function fetchProblems(): Promise { throw new Error("Failed to fetch problems"); } - const data = await response.json(); - return data; + const data: ProblemApiResponse[] = await response.json(); + + // Map Backend Data (MongoDB _id) to Frontend Interface + return data.map((item: ProblemApiResponse): Problem => ({ + id: item._id, // Map _id to id + slug: item.slug, // Important for routing + title: item.title, + difficulty: item.difficulty, + // Fallback for fields not yet in backend + category: item.tags?.[0] || "General", + acceptance: 0, // Backend doesn't calculate this yet + status: "unsolved", // Needs user-specific fetch + tags: item.tags || [], + })); } catch (error) { console.error("Error fetching problems:", error); - throw error; + return []; } } -export async function fetchProblemById(problemId: string): Promise { +export async function fetchProblemBySlug( + slug: string +): Promise { try { - const response = await fetch(`${API_BASE_URL}/problems/${problemId}`, { + const response = await fetch(`${API_BASE_URL}/problems/${slug}`, { method: "GET", - headers: { - "Content-Type": "application/json", - }, + headers: { "Content-Type": "application/json" }, }); + if (!response.ok) throw new Error("Failed to fetch problem"); - if (!response.ok) { - throw new Error("Failed to fetch problem"); - } - - const data = await response.json(); - return data; + const problem: ProblemDetail = await response.json(); + return problem; } catch (error) { console.error("Error fetching problem:", error); throw error; } } -export async function fetchProblemsByCategory( - category: string -): Promise { - try { - const response = await fetch( - `${API_BASE_URL}/problems?category=${encodeURIComponent(category)}`, - { - method: "GET", - headers: { - "Content-Type": "application/json", - }, - } - ); - - if (!response.ok) { - throw new Error("Failed to fetch problems by category"); - } - - const data = await response.json(); - return data; - } catch (error) { - console.error("Error fetching problems by category:", error); - throw error; - } -} - -export async function fetchUserStats(): Promise { - try { - const response = await fetch(`${API_BASE_URL}/user/stats`, { - method: "GET", - headers: { - "Content-Type": "application/json", - }, - }); - - if (!response.ok) { - throw new Error("Failed to fetch user stats"); - } - - const data = await response.json(); - return data; - } catch (error) { - console.error("Error fetching user stats:", error); - throw error; - } -} - export async function submitSolution( problemId: string, - solution: string -): Promise { - try { - const response = await fetch( - `${API_BASE_URL}/problems/${problemId}/submit`, - { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify({ solution }), - } - ); - - if (!response.ok) { - throw new Error("Failed to submit solution"); - } - - const data = await response.json(); - return data; - } catch (error) { - console.error("Error submitting solution:", error); - throw error; - } -} - -export async function logout(): Promise { - try { - const response = await fetch(`${API_BASE_URL}/auth/logout`, { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - }); - - if (!response.ok) { - throw new Error("Failed to logout"); - } - } catch (error) { - console.error("Error during logout:", error); - throw error; - } + code: string, + languageId: number, + token: string +): Promise { + const response = await fetch(`${API_BASE_URL}/submissions`, { + method: "POST", + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${token}`, // Secure submission + }, + body: JSON.stringify({ problemId, code, languageId }), + }); + + const result: SubmissionResult = await response.json(); + return result; } diff --git a/src/types/index.ts b/src/types/index.ts index 05623ea..cedd3c9 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -1,5 +1,6 @@ export interface Problem { id: string; + slug?: string; // Added for routing title: string; difficulty: "Easy" | "Medium" | "Hard"; category: string; @@ -12,4 +13,4 @@ export interface FilterState { level: string; category: string; statusFilter: "all" | "todo" | "solved"; -} +} \ No newline at end of file