From 645f73664d7fc3a0a3991e7119855b991d8e5e29 Mon Sep 17 00:00:00 2001 From: biru-codeastromer Date: Wed, 3 Dec 2025 02:01:38 +0530 Subject: [PATCH 1/2] Connect Problems UI to Node API and add slug-based solving flow --- package-lock.json | 72 ++++++++++++++++ package.json | 1 + src/app/problems/[slug]/page.tsx | 136 ++++++++++++++++++++++++++++++ src/app/problems/page.tsx | 11 ++- src/lib/api.ts | 140 ++++++++----------------------- src/types/index.ts | 3 +- 6 files changed, 256 insertions(+), 107 deletions(-) create mode 100644 src/app/problems/[slug]/page.tsx 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..f0ae874 --- /dev/null +++ b/src/app/problems/[slug]/page.tsx @@ -0,0 +1,136 @@ +"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 } 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 () => { + 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: string) => ( + + {tag} + + ))} +
+
+ +
+

{problem.description}

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

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 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..0824a25 100644 --- a/src/lib/api.ts +++ b/src/lib/api.ts @@ -1,7 +1,7 @@ import { Problem } from "@/types"; -const API_BASE_URL = - process.env.NEXT_PUBLIC_API_URL || "http://localhost:8000/api"; +// Update to match your backend port (5001) +const API_BASE_URL = process.env.NEXT_PUBLIC_API_URL || "http://localhost:5001/api"; export async function fetchProblems(): Promise { try { @@ -9,6 +9,7 @@ export async function fetchProblems(): Promise { method: "GET", headers: { "Content-Type": "application/json", + // Add Authorization header if you implement protected problem lists later }, }); @@ -17,123 +18,52 @@ export async function fetchProblems(): Promise { } const data = await response.json(); - return data; + + // Map Backend Data (MongoDB _id) to Frontend Interface + return data.map((item: any) => ({ + 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"); - } - - const data = await response.json(); - return data; + if (!response.ok) throw new Error("Failed to fetch problem"); + return await response.json(); } 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 }), + }); + return await response.json(); } 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 From 5cc8f09144e7867cef174d1087ecfa38a1a8f6c3 Mon Sep 17 00:00:00 2001 From: biru-codeastromer Date: Wed, 3 Dec 2025 02:08:16 +0530 Subject: [PATCH 2/2] fixing linting --- src/app/problems/[slug]/page.tsx | 101 +++++++++++++++++++++++-------- src/lib/api.ts | 65 ++++++++++++++++---- 2 files changed, 129 insertions(+), 37 deletions(-) diff --git a/src/app/problems/[slug]/page.tsx b/src/app/problems/[slug]/page.tsx index f0ae874..81d1f9d 100644 --- a/src/app/problems/[slug]/page.tsx +++ b/src/app/problems/[slug]/page.tsx @@ -5,27 +5,38 @@ import { useParams } from "next/navigation"; import Editor from "@monaco-editor/react"; import Navbar from "../../components/Navbar"; import Sidebar from "../../components/Sidebar"; -import { fetchProblemBySlug, submitSolution } from "@/lib/api"; +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 [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); + 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') || ""; - + const token = localStorage.getItem("accessToken") || ""; + try { const result = await submitSolution(problem._id, code, 71, token); // 71 is Python setOutput(result); @@ -42,23 +53,37 @@ export default function ProblemSolvePage() {
setIsSidebarOpen(false)} /> -
+
{/* Left Panel: Problem Description */}
-

{problem.title}

+

+ {problem.title} +

- + {problem.difficulty} - {problem.tags?.map((tag: string) => ( - + {problem.tags?.map((tag) => ( + {tag} ))} @@ -70,13 +95,24 @@ export default function ProblemSolvePage() {
{/* Sample Test Cases Display */} - {problem.sampleTestCases?.map((tc: any, idx: number) => ( + {problem.sampleTestCases?.map((tc, idx) => (
-

Example {idx + 1}

+

+ Example {idx + 1} +

-
Input: {tc.input}
-
Output: {tc.output}
- {tc.explanation &&
Explanation: {tc.explanation}
} +
+ Input: {tc.input} +
+
+ Output: {tc.output} +
+ {tc.explanation && ( +
+ Explanation:{" "} + {tc.explanation} +
+ )}
))} @@ -86,14 +122,16 @@ export default function ProblemSolvePage() {
Python 3.8 -
@@ -115,15 +153,26 @@ export default function ProblemSolvePage() { {/* Output Console */} {output && (
-

Result

+

+ Result +

{output.status === "Accepted" ? (
- ✓ Accepted Runtime: {output.runtime}s + ✓ Accepted{" "} + {typeof output.runtime === "number" && ( + + Runtime: {output.runtime}s + + )}
) : (
✗ {output.status} - {output.errorMessage &&
{output.errorMessage}
} + {output.errorMessage && ( +
+ {output.errorMessage} +
+ )}
)}
diff --git a/src/lib/api.ts b/src/lib/api.ts index 0824a25..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:5001/api"; +const API_BASE_URL = + 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 { @@ -17,19 +54,19 @@ export async function fetchProblems(): Promise { throw new Error("Failed to fetch problems"); } - const data = await response.json(); - + const data: ProblemApiResponse[] = await response.json(); + // Map Backend Data (MongoDB _id) to Frontend Interface - return data.map((item: any) => ({ + 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", + category: item.tags?.[0] || "General", acceptance: 0, // Backend doesn't calculate this yet status: "unsolved", // Needs user-specific fetch - tags: item.tags || [] + tags: item.tags || [], })); } catch (error) { console.error("Error fetching problems:", error); @@ -37,14 +74,18 @@ export async function fetchProblems(): Promise { } } -export async function fetchProblemBySlug(slug: string): Promise { +export async function fetchProblemBySlug( + slug: string +): Promise { try { const response = await fetch(`${API_BASE_URL}/problems/${slug}`, { method: "GET", headers: { "Content-Type": "application/json" }, }); if (!response.ok) throw new Error("Failed to fetch problem"); - return await response.json(); + + const problem: ProblemDetail = await response.json(); + return problem; } catch (error) { console.error("Error fetching problem:", error); throw error; @@ -56,14 +97,16 @@ export async function submitSolution( code: string, languageId: number, token: string -): Promise { +): Promise { const response = await fetch(`${API_BASE_URL}/submissions`, { method: "POST", headers: { "Content-Type": "application/json", - "Authorization": `Bearer ${token}` // Secure submission + Authorization: `Bearer ${token}`, // Secure submission }, body: JSON.stringify({ problemId, code, languageId }), }); - return await response.json(); + + const result: SubmissionResult = await response.json(); + return result; }