algospaced-dsa / algospaced-ui /src /components /PythonChallenge.tsx
iamfebin's picture
Configure AlgoSpaced for Hugging Face Spaces deployment
aaa634c
Raw
History Blame Contribute Delete
9.58 kB
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>
);
}