iamfebin's picture
Configure AlgoSpaced for Hugging Face Spaces deployment
aaa634c
Raw
History Blame Contribute Delete
18.5 kB
import { useEffect, useState } from 'react';
import { supabase } from '../supabaseClient';
import { ExternalLink, Search, CheckCircle, XCircle, ArrowLeft, RefreshCw, Star, Info } from 'lucide-react';
const JOURNEY_SEQUENCE = [
217, 242, 1, 49, 347, 271, 238, 36, 128, 125, 167, 15, 11, 42, 121, 3, 424, 567, 76, 239, 20,
155, 150, 739, 853, 84, 704, 74, 875, 153, 33, 981, 4, 206, 21, 141, 143, 19, 138, 2, 287,
146, 23, 25, 226, 104, 543, 110, 100, 572, 235, 102, 199, 1448, 98, 230, 105, 124, 297, 703,
1046, 973, 215, 621, 355, 295, 78, 39, 40, 46, 90, 22, 79, 131, 17, 51, 208, 211, 212, 200,
695, 133, 286, 994, 417, 130, 207, 210, 261, 323, 684, 127, 743, 332, 1584, 778, 269, 787,
70, 746, 198, 213, 5, 647, 91, 322, 152, 139, 300, 416, 62, 1143, 309, 518, 494, 97, 329,
115, 72, 312, 10, 53, 55, 45, 134, 846, 1899, 763, 678, 57, 56, 435, 252, 253, 1851, 48,
54, 73, 202, 66, 50, 43, 2013, 136, 191, 338, 190, 268, 371, 7
];
interface JourneyPathProps {
onSelectProblemForReview: (problemId: string) => void;
}
export default function JourneyPath({ onSelectProblemForReview }: JourneyPathProps) {
const [completions, setCompletions] = useState<Set<number>>(new Set());
const [titles, setTitles] = useState<Record<number, string>>({});
const [loading, setLoading] = useState(true);
const [selectedNum, setSelectedNum] = useState<number | null>(null);
const [details, setDetails] = useState<any[]>([]);
const [modalLoading, setModalLoading] = useState(false);
const [toggleLoading, setToggleLoading] = useState(false);
const fetchData = async () => {
try {
const { data: { session } } = await supabase.auth.getSession();
if (!session) return;
const headers = { 'Authorization': `Bearer ${session.access_token}` };
const [progRes, titlesRes] = await Promise.all([
fetch(`${import.meta.env.VITE_API_BASE_URL}/api/journey-progress`, { headers }),
fetch(`${import.meta.env.VITE_API_BASE_URL}/api/journey-progress/titles`, { headers })
]);
if (progRes.ok && titlesRes.ok) {
const progData = await progRes.json();
const titlesData = await titlesRes.json();
setCompletions(new Set(progData));
setTitles(titlesData);
}
} catch (err) {
console.error('Error fetching journey data:', err);
} finally {
setLoading(false);
}
};
useEffect(() => {
fetchData();
}, []);
const handleNodeClick = async (num: number) => {
setSelectedNum(num);
setDetails([]);
setModalLoading(true);
try {
const { data: { session } } = await supabase.auth.getSession();
if (!session) return;
const res = await fetch(`${import.meta.env.VITE_API_BASE_URL}/api/reviews/problems/${num}`, {
headers: { 'Authorization': `Bearer ${session.access_token}` }
});
if (res.ok) {
const data = await res.json();
setDetails(data);
}
} catch (err) {
console.error('Error fetching problem details:', err);
} finally {
setModalLoading(false);
}
};
const handleToggleCompletion = async (num: number) => {
setToggleLoading(true);
try {
const { data: { session } } = await supabase.auth.getSession();
if (!session) return;
const res = await fetch(`${import.meta.env.VITE_API_BASE_URL}/api/journey-progress/toggle`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${session.access_token}`
},
body: JSON.stringify({ problem_number: num })
});
if (res.ok) {
const updated = new Set(completions);
if (updated.has(num)) {
updated.delete(num);
} else {
updated.add(num);
}
setCompletions(updated);
}
} catch (err) {
console.error('Error toggling progress:', err);
} finally {
setToggleLoading(false);
}
};
// Compute stats
const total = JOURNEY_SEQUENCE.length;
const completedCount = JOURNEY_SEQUENCE.filter(num => completions.has(num)).length;
const percentage = total > 0 ? Math.round((completedCount / total) * 100) : 0;
if (loading) {
return (
<div className="w-full h-full flex items-center justify-center bg-[#fdf8e1] text-ink-black">
<div className="flex flex-col items-center gap-3">
<RefreshCw className="w-8 h-8 text-primary animate-spin" />
<span className="font-arcade text-[10px] tracking-wide">COMPILING MAP...</span>
</div>
</div>
);
}
// Determine active node: first node in sequence that is not completed
const activeNode = JOURNEY_SEQUENCE.find(num => !completions.has(num)) || JOURNEY_SEQUENCE[0];
return (
<div className="w-full h-full bg-[#fdf8e1] p-6 overflow-y-auto custom-scrollbar flex flex-col relative">
<div className="flex flex-col gap-4">
{/* Header progress info */}
<div className="flex flex-col md:flex-row items-center justify-between p-4 bg-white border-[3px] border-ink-black shadow-[4px_4px_0px_0px_rgba(30,41,59,1)] gap-4 select-none">
<div>
<h3 className="font-headline-md font-bold text-ink-black text-sm m-0">MAP PROGRESS STATUS</h3>
<span className="text-[10px] font-arcade text-trash-gray font-bold uppercase mt-1 block">Leitner path completion metrics</span>
</div>
<div className="flex items-center gap-4">
<span className="font-arcade text-[10px] font-bold text-ink-black">{percentage}% DONE</span>
<div className="w-48 h-6 bg-slate-100 border-[3px] border-ink-black relative overflow-hidden">
<div
className="absolute inset-y-0 left-0 bg-[#0ea5e9] border-r-[3px] border-ink-black transition-all duration-500"
style={{ width: `${percentage}%` }}
/>
</div>
</div>
</div>
{/* Milestone path visualization */}
<div className="bg-white border-[3px] border-ink-black shadow-[4px_4px_0px_0px_rgba(30,41,59,1)] relative p-8 mt-4 overflow-hidden min-h-[700px] flex flex-col items-center">
{/* Timeline Center Sine Path via SVG */}
<svg className="absolute inset-y-0 w-full h-full pointer-events-none" preserveAspectRatio="none" viewBox="0 0 100 1000">
<path
d="M 50,0 Q 70,50 30,100 T 50,200 T 70,300 T 30,400 T 50,500 T 70,600 T 30,700 T 50,800 T 70,900 T 50,1000"
fill="none"
opacity="0.3"
stroke="#1E293B"
strokeDasharray="5,15"
strokeLinecap="round"
strokeWidth="6"
/>
</svg>
{/* Nodes Container */}
<div className="relative z-10 flex flex-col items-center gap-10 w-full max-w-sm">
{JOURNEY_SEQUENCE.map((num, index) => {
const isCompleted = completions.has(num);
const isSynced = num in titles;
const title = titles[num] || 'Unsynced Note';
const isActive = num === activeNode;
// Sine wave displacement
const xOffset = Math.sin(index * 0.8) * 75;
return (
<div
key={num}
className="relative group flex items-center justify-center"
style={{ transform: `translateX(${xOffset}px)` }}
>
{/* Tooltip */}
<div className="absolute bottom-[115%] left-1/2 -translate-x-1/2 bg-paper-white text-ink-black border-[3px] border-ink-black text-[9px] font-arcade px-3 py-2 rounded-xl whitespace-nowrap shadow-[4px_4px_0px_0px_#1E293B] opacity-0 group-hover:opacity-100 transition-all duration-200 scale-90 group-hover:scale-100 pointer-events-none z-30">
<span className="font-bold text-primary mr-1">#{num}</span>
<span>{title.toUpperCase()}</span>
{!isSynced && <span className="ml-2 text-red-500 font-bold">(LOCKED)</span>}
</div>
{/* Circular / Rounded Square Node */}
<button
onClick={() => handleNodeClick(num)}
className={`w-14 h-14 border-[3px] border-ink-black flex items-center justify-center font-arcade font-bold text-xs cursor-pointer transition-all duration-200 hover:scale-110 active:scale-95 ${
isCompleted
? 'bg-grass-green text-ink-black shadow-[3px_3px_0px_0px_#1E293B] rounded-lg'
: isActive
? 'bg-[#ffe24c] text-ink-black shadow-[4px_4px_0px_0px_#F472B6] rounded-xl animate-bounce border-dashed'
: isSynced
? 'bg-[#c0e8ff] text-ink-black shadow-[3px_3px_0px_0px_#1E293B] rounded-lg'
: 'bg-slate-200 text-trash-gray opacity-60 shadow-[1px_1px_0px_0px_#1E293B] rounded-lg'
}`}
>
{isCompleted ? '✓' : isActive ? '▶' : num}
</button>
</div>
);
})}
</div>
</div>
</div>
{/* Modal Dialog (Styled as a Retro OS System Dialog) */}
{selectedNum !== null && (
<div className="fixed inset-0 bg-black/50 backdrop-blur-xs flex items-center justify-center z-[100] p-4 animate-fade-in">
<div className="w-full max-w-md bg-paper-white border-[4px] border-ink-black p-0 shadow-[8px_8px_0px_0px_rgba(30,41,59,1)] relative select-none">
{/* Title Bar */}
<div className="bg-[#0ea5e9] border-b-[4px] border-ink-black px-4 py-2 flex justify-between items-center text-white font-bold text-sm">
<div className="flex items-center gap-2">
<span className="material-symbols-outlined text-sm font-bold">info</span>
<span>Problem_Detail.sys</span>
</div>
<button
onClick={() => setSelectedNum(null)}
className="w-6 h-6 bg-highlight-pink text-white border-2 border-ink-black flex items-center justify-center hover:bg-rose-600 active:translate-y-0.5 shadow-[1px_1px_0px_0px_rgba(30,41,59,1)] active:shadow-none transition-all cursor-pointer font-bold"
>
&times;
</button>
</div>
{/* Dialog Content */}
<div className="p-6 bg-[#fdf8e1] space-y-4">
<div className="pb-3 border-b-2 border-dashed border-ink-black">
<span className="text-[8px] font-arcade font-bold text-trash-gray uppercase block">Milestone Index</span>
<h3 className="text-xl font-bold font-headline-md text-ink-black mt-1">LeetCode Problem #{selectedNum}</h3>
</div>
{modalLoading ? (
<div className="py-8 flex flex-col items-center justify-center gap-3">
<RefreshCw className="w-6 h-6 text-primary animate-spin" />
<span className="text-[8px] font-arcade font-bold text-trash-gray uppercase">FETCHING DATA...</span>
</div>
) : details.length > 0 ? (
<div className="space-y-4">
{details.map((prob) => (
<div key={prob.id} className="p-4 bg-white border-[3px] border-ink-black shadow-[3px_3px_0px_0px_rgba(30,41,59,1)] space-y-3">
<div className="flex justify-between items-start gap-2">
<div>
<h4 className="font-bold text-ink-black text-sm font-headline-md leading-tight m-0">{prob.name}</h4>
<span className="text-[8px] font-arcade text-trash-gray font-bold uppercase mt-1 block">Pattern: {prob.pattern}</span>
</div>
<span className={`text-[8px] font-arcade font-bold px-2 py-0.5 border-2 border-ink-black ${
prob.difficulty === 'Easy' ? 'bg-grass-green text-ink-black' :
prob.difficulty === 'Medium' ? 'bg-secondary-container text-ink-black' :
'bg-highlight-pink text-white'
}`}>
{prob.difficulty.toUpperCase()}
</span>
</div>
<div className="flex gap-4 text-xs font-bold text-ink-black">
<div>Time: <code className="bg-[#1E293B] text-[#4ADE80] font-code-mono px-1.5 py-0.5 border border-ink-black">{prob.optimal_time}</code></div>
<div>Space: <code className="bg-[#1E293B] text-[#4ADE80] font-code-mono px-1.5 py-0.5 border border-ink-black">{prob.optimal_space}</code></div>
</div>
<div className="flex flex-col sm:flex-row gap-2 pt-2 border-t border-slate-200">
<a
href={prob.google_doc_url}
target="_blank"
rel="noreferrer"
className="flex-1 flex items-center justify-center gap-1.5 py-2 border-[3px] border-ink-black bg-white hover:bg-slate-100 text-xs font-bold text-ink-black shadow-[2px_2px_0px_0px_#1E293B] active:translate-y-0.5 active:shadow-none transition-all cursor-pointer text-center"
>
<ExternalLink className="w-3.5 h-3.5" />
Google Doc
</a>
<button
onClick={() => {
onSelectProblemForReview(prob.id);
setSelectedNum(null);
}}
className="flex-1 flex items-center justify-center gap-1.5 py-2 border-[3px] border-ink-black bg-[#ffe24c] hover:bg-yellow-300 text-xs font-bold text-ink-black shadow-[2px_2px_0px_0px_#1E293B] active:translate-y-0.5 active:shadow-none transition-all cursor-pointer text-center"
>
<Star className="w-3.5 h-3.5" />
Review Problem
</button>
</div>
</div>
))}
</div>
) : (
<div className="p-4 bg-white border-[3px] border-ink-black shadow-[3px_3px_0px_0px_rgba(30,41,59,1)] space-y-4">
<div className="flex items-start gap-3 text-xs font-bold text-ink-black leading-relaxed">
<Info className="w-5 h-5 text-amber-500 shrink-0 mt-0.5" />
<div>
<h4 className="font-bold text-ink-black m-0">No Local Note Synced</h4>
<p className="text-[10px] text-trash-gray mt-1 leading-normal font-semibold">
You haven't synced notes for LeetCode #{selectedNum} yet. Click "Sync Google Drive" to import your progress notes.
</p>
</div>
</div>
<div className="space-y-2 pt-2 border-t border-slate-200">
<span className="text-[8px] font-arcade text-trash-gray uppercase font-bold block">External Search Shortcuts:</span>
<a
href={`https://leetcode.com/problemset/?search=${selectedNum}`}
target="_blank"
rel="noreferrer"
className="w-full flex items-center justify-between p-2.5 bg-white border-[3px] border-ink-black text-xs font-bold text-ink-black hover:bg-slate-100 shadow-[2px_2px_0px_0px_#1E293B] active:translate-y-0.5 active:shadow-none transition-all cursor-pointer"
>
<span className="flex items-center gap-2">
<ExternalLink className="w-4 h-4 text-emerald-500" />
Search LeetCode
</span>
<ArrowLeft className="w-3.5 h-3.5 rotate-180 text-trash-gray" />
</a>
<a
href={`https://www.google.com/search?q=leetcode+${selectedNum}`}
target="_blank"
rel="noreferrer"
className="w-full flex items-center justify-between p-2.5 bg-white border-[3px] border-ink-black text-xs font-bold text-ink-black hover:bg-slate-100 shadow-[2px_2px_0px_0px_#1E293B] active:translate-y-0.5 active:shadow-none transition-all cursor-pointer"
>
<span className="flex items-center gap-2">
<Search className="w-4 h-4 text-primary" />
Search Google
</span>
<ArrowLeft className="w-3.5 h-3.5 rotate-180 text-trash-gray" />
</a>
</div>
</div>
)}
{/* Completion Toggle */}
<div className="mt-4 pt-3 border-t-2 border-dashed border-ink-black flex flex-col gap-2">
<span className="text-[8px] font-arcade text-trash-gray font-bold uppercase block">Quick Action</span>
<button
onClick={() => handleToggleCompletion(selectedNum)}
disabled={toggleLoading}
className={`w-full py-3 border-[3px] border-ink-black font-arcade text-[10px] font-bold tracking-wide uppercase flex items-center justify-center gap-2 cursor-pointer transition-all shadow-[3px_3px_0px_0px_#1E293B] active:translate-y-0.5 active:shadow-none disabled:opacity-50 ${
completions.has(selectedNum)
? 'bg-red-100 text-red-600 hover:bg-red-200'
: 'bg-emerald-100 text-emerald-700 hover:bg-emerald-200'
}`}
>
{completions.has(selectedNum) ? (
<>
<XCircle className="w-4.5 h-4.5" />
UNMARK AS COMPLETED
</>
) : (
<>
<CheckCircle className="w-4.5 h-4.5" />
MARK AS COMPLETED
</>
)}
</button>
</div>
</div>
</div>
</div>
)}
</div>
);
}