Spaces:
Sleeping
Sleeping
| import { useState, useEffect } from 'react'; | |
| import { supabase } from '../supabaseClient'; | |
| interface PythonChallengeData { | |
| title: string; | |
| description: string; | |
| difficulty: string; | |
| points: number; | |
| starter_code: string; | |
| completed: boolean; | |
| total_score: number; | |
| } | |
| export default function PythonChallenge() { | |
| const [challenge, setChallenge] = useState<PythonChallengeData | null>(null); | |
| const [code, setCode] = useState(''); | |
| const [submitting, setSubmitting] = useState(false); | |
| const [results, setResults] = useState<any[]>([]); | |
| const [error, setError] = useState(''); | |
| const [loading, setLoading] = useState(true); | |
| const [globalScore, setGlobalScore] = useState(0); | |
| const fetchChallenge = async () => { | |
| setLoading(true); | |
| setError(''); | |
| try { | |
| const { data: { session } } = await supabase.auth.getSession(); | |
| if (!session) { | |
| setError('Unauthenticated session'); | |
| return; | |
| } | |
| const res = await fetch(`${import.meta.env.VITE_API_BASE_URL}/api/challenges/python/daily`, { | |
| headers: { 'Authorization': `Bearer ${session.access_token}` } | |
| }); | |
| if (res.ok) { | |
| const data = await res.json(); | |
| setChallenge(data); | |
| setCode(data.starter_code); | |
| setGlobalScore(data.total_score); | |
| } else { | |
| setError('Failed to load challenge from server.'); | |
| } | |
| } catch { | |
| setError('Connection timed out.'); | |
| } finally { | |
| setLoading(false); | |
| } | |
| }; | |
| useEffect(() => { | |
| fetchChallenge(); | |
| }, []); | |
| const handleSubmit = async () => { | |
| setSubmitting(true); | |
| setError(''); | |
| setResults([]); | |
| try { | |
| const { data: { session } } = await supabase.auth.getSession(); | |
| if (!session) { | |
| setError('Unauthenticated session'); | |
| return; | |
| } | |
| const res = await fetch(`${import.meta.env.VITE_API_BASE_URL}/api/challenges/python/submit`, { | |
| method: 'POST', | |
| headers: { | |
| 'Content-Type': 'application/json', | |
| 'Authorization': `Bearer ${session.access_token}` | |
| }, | |
| body: JSON.stringify({ code }) | |
| }); | |
| if (res.ok) { | |
| const data = await res.json(); | |
| if (data.success) { | |
| setResults(data.results); | |
| if (data.all_passed) { | |
| setGlobalScore(data.total_score); | |
| setChallenge(prev => prev ? { ...prev, completed: true } : null); | |
| } | |
| } else { | |
| setError(data.error); | |
| } | |
| } else { | |
| setError('Submission failed at server host.'); | |
| } | |
| } catch { | |
| setError('Network connection lost.'); | |
| } finally { | |
| setSubmitting(false); | |
| } | |
| }; | |
| if (loading) { | |
| return ( | |
| <div className="w-full h-full bg-paper-white p-6 flex flex-col items-center justify-center font-arcade text-[10px]"> | |
| <div className="animate-bounce mb-2">LOADING CHALLENGE...</div> | |
| <div className="w-48 h-4 bg-white border-[3px] border-ink-black overflow-hidden relative"> | |
| <div className="absolute inset-y-0 left-0 bg-[#9d4edd] w-2/3 animate-pulse border-r-2 border-ink-black" /> | |
| </div> | |
| </div> | |
| ); | |
| } | |
| if (!challenge) { | |
| return ( | |
| <div className="w-full h-full bg-paper-white p-6 flex flex-col items-center justify-center font-sans text-center gap-3"> | |
| <span className="material-symbols-outlined text-red-500 text-4xl">error</span> | |
| <div className="font-bold text-ink-black text-sm">{error || 'No active challenge found.'}</div> | |
| <button | |
| onClick={fetchChallenge} | |
| className="px-3 py-1.5 bg-yellow-400 hover:bg-yellow-300 border-[3px] border-ink-black text-xs font-bold font-window-title cursor-pointer shadow-[2px_2px_0px_rgba(0,0,0,1)] active:translate-y-0.5 active:shadow-none" | |
| > | |
| RETRY | |
| </button> | |
| </div> | |
| ); | |
| } | |
| return ( | |
| <div className="w-full h-full bg-paper-white text-ink-black flex flex-col overflow-hidden text-xs md:text-sm"> | |
| {/* Workspace Body */} | |
| <div className="flex-1 overflow-y-auto p-4 custom-scrollbar flex flex-col gap-4"> | |
| {/* Info Header */} | |
| <div className="flex justify-between items-center bg-slate-100 border-[3px] border-ink-black p-3"> | |
| <div className="flex gap-2"> | |
| <span className={`px-2 py-0.5 border-2 border-ink-black font-bold uppercase text-[9px] ${ | |
| challenge.difficulty === 'Easy' ? 'bg-green-200 text-green-800' : | |
| challenge.difficulty === 'Medium' ? 'bg-yellow-200 text-yellow-800' : | |
| 'bg-red-200 text-red-800' | |
| }`}> | |
| {challenge.difficulty} | |
| </span> | |
| <span className="bg-blue-100 text-blue-800 px-2 py-0.5 border-2 border-ink-black font-bold text-[9px]"> | |
| +{challenge.points} PTS | |
| </span> | |
| {challenge.completed ? ( | |
| <span className="bg-emerald-500 text-white px-2 py-0.5 border-2 border-ink-black font-bold text-[9px]"> | |
| COMPLETED | |
| </span> | |
| ) : ( | |
| <span className="bg-rose-500 text-white px-2 py-0.5 border-2 border-ink-black font-bold text-[9px]"> | |
| INCOMPLETE | |
| </span> | |
| )} | |
| </div> | |
| <div className="font-arcade text-[8px] text-purple-700 font-bold bg-white border-2 border-ink-black px-2 py-0.5 shadow-[1px_1px_0_0_#1E293B]"> | |
| TOTAL SCORE: {globalScore} | |
| </div> | |
| </div> | |
| {/* Description */} | |
| <div className="border-2 border-ink-black bg-slate-50 p-3 rounded font-sans leading-relaxed text-xs"> | |
| <h3 className="font-bold border-b border-slate-300 pb-1 mb-2 uppercase tracking-wide text-primary-purple flex items-center gap-1"> | |
| <span className="material-symbols-outlined text-sm">description</span> | |
| Problem: {challenge.title} | |
| </h3> | |
| <p className="whitespace-pre-wrap">{challenge.description}</p> | |
| </div> | |
| {/* Code Editor */} | |
| <div className="flex flex-col flex-1 min-h-[220px]"> | |
| <div className="flex justify-between items-center mb-1"> | |
| <span className="font-bold text-[10px] tracking-wide uppercase flex items-center gap-1"> | |
| <span className="material-symbols-outlined text-sm text-purple-600">code</span> | |
| Write Python Solution: | |
| </span> | |
| <button | |
| onClick={() => setCode(challenge.starter_code)} | |
| className="text-[9px] underline text-blue-600 hover:text-blue-800 cursor-pointer font-sans" | |
| > | |
| Reset Starter Code | |
| </button> | |
| </div> | |
| <textarea | |
| value={code} | |
| onChange={(e) => setCode(e.target.value)} | |
| className="flex-1 w-full bg-slate-900 text-[#4ADE80] font-mono p-3 border-[3px] border-ink-black rounded outline-none focus:ring-2 focus:ring-primary-purple text-xs leading-relaxed resize-none" | |
| placeholder="# Write your python code here" | |
| spellCheck="false" | |
| /> | |
| </div> | |
| {/* Submit Bar */} | |
| <div className="shrink-0"> | |
| <button | |
| disabled={submitting} | |
| onClick={handleSubmit} | |
| className="w-full bg-highlight-pink text-white font-window-title font-bold py-2 px-4 border-[3px] border-ink-black shadow-[3px_3px_0px_0px_rgba(30,41,59,1)] active:translate-y-0.5 active:shadow-none hover:bg-pink-400 transition-all disabled:opacity-50 cursor-pointer text-center text-xs" | |
| > | |
| {submitting ? 'EXECUTING TEST CASES...' : 'RUN SOLUTION SUBMISSION'} | |
| </button> | |
| </div> | |
| {/* Error Console */} | |
| {error && ( | |
| <div className="border-[3px] border-red-500 bg-red-50 text-red-800 p-3 font-mono text-xs whitespace-pre-wrap"> | |
| <div className="font-bold mb-1">COMPILER / RUNTIME ERROR:</div> | |
| {error} | |
| </div> | |
| )} | |
| {/* Test Results */} | |
| {results.length > 0 && ( | |
| <div className="border-[3px] border-ink-black bg-slate-100 p-3"> | |
| <div className="font-bold text-[10px] border-b border-slate-300 pb-1 mb-2 uppercase">Test Case Executions:</div> | |
| <div className="space-y-1.5 font-mono text-xs"> | |
| {results.map((res, index) => ( | |
| <div key={index} className="flex flex-col p-2 border border-slate-300 bg-white"> | |
| <div className="flex justify-between"> | |
| <span className="font-bold">Test Case {index + 1}:</span> | |
| <span className={`font-bold uppercase ${res.status === 'passed' ? 'text-green-600' : 'text-red-600'}`}> | |
| {res.status} | |
| </span> | |
| </div> | |
| <div className="text-[10px] text-slate-500 mt-1"> | |
| <div>Input: {JSON.stringify(res.input)}</div> | |
| {res.status === 'passed' && <div>Output: {JSON.stringify(res.got)}</div>} | |
| {res.status === 'failed' && ( | |
| <div className="text-red-700"> | |
| Expected: {JSON.stringify(res.expected)} | Got: {JSON.stringify(res.got)} | |
| </div> | |
| )} | |
| {res.status === 'error' && ( | |
| <div className="text-red-700 font-semibold"> | |
| Error: {res.error} | |
| </div> | |
| )} | |
| </div> | |
| </div> | |
| ))} | |
| </div> | |
| </div> | |
| )} | |
| </div> | |
| </div> | |
| ); | |
| } | |