Spaces:
Running
Running
File size: 6,507 Bytes
74626f2 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 | import React, { useState } from 'react';
import { LearningActivity, LearningMap } from '../types';
import { CheckCircle, Clock, Lock, ArrowDown, Target } from 'lucide-react';
interface LearningRoadmapProps {
learningMap: LearningMap;
onActivitySelect: (activityId: string) => void;
}
export const LearningRoadmap: React.FC<LearningRoadmapProps> = ({
learningMap,
onActivitySelect
}) => {
const [selectedActivity, setSelectedActivity] = useState<string | null>(null);
const isActivityUnlocked = (activity: LearningActivity) => {
if (activity.prerequisites.length === 0) return true;
return activity.prerequisites.every(prereqId =>
learningMap.activities.find(a => a.id === prereqId)?.completed
);
};
const getActivityIcon = (activity: LearningActivity) => {
if (activity.completed) {
return <CheckCircle className="w-5 h-5 text-green-500" />;
} else if (isActivityUnlocked(activity)) {
return <Target className="w-5 h-5 text-blue-500" />;
} else {
return <Lock className="w-5 h-5 text-gray-400" />;
}
};
const getActivityStyle = (activity: LearningActivity) => {
if (activity.completed) {
return 'bg-green-50 border-green-200 text-green-800 shadow-sm';
} else if (isActivityUnlocked(activity)) {
return 'bg-blue-50 border-blue-200 text-blue-800 hover:bg-blue-100 cursor-pointer shadow-sm hover:shadow-md transform hover:scale-[1.02] transition-all duration-200';
} else {
return 'bg-gray-50 border-gray-200 text-gray-500';
}
};
const handleActivityClick = (activity: LearningActivity) => {
if (isActivityUnlocked(activity) && !activity.completed) {
setSelectedActivity(activity.id);
onActivitySelect(activity.id);
}
};
return (
<div className="bg-white p-3 lg:p-4 rounded-lg shadow-lg h-full flex flex-col">
<div className="mb-6">
<h2 className="text-sm lg:text-base font-semibold text-gray-800 mb-2">Learning Roadmap</h2>
<div className="flex items-center justify-between">
<p className="text-xs text-gray-600">Track your learning journey</p>
<div className="flex items-center space-x-2">
<div className="text-xs text-gray-600">Progress:</div>
<div className="text-xs font-semibold text-blue-600">
{Math.round(learningMap.progressPercentage)}%
</div>
</div>
</div>
<div className="w-full bg-gray-200 rounded-full h-2 mt-2">
<div
className="bg-gradient-to-r from-blue-500 to-blue-600 h-2 rounded-full transition-all duration-500"
style={{ width: `${learningMap.progressPercentage}%` }}
></div>
</div>
</div>
<div className="space-y-3 flex-1 overflow-y-auto">
{learningMap.activities.map((activity, index) => (
<div key={activity.id} className="relative">
<div
className={`
p-4 rounded-lg border-2 transition-all duration-300
${getActivityStyle(activity)}
${selectedActivity === activity.id ? 'ring-2 ring-blue-400 ring-offset-2' : ''}
`}
onClick={() => handleActivityClick(activity)}
>
<div className="flex items-start space-x-2">
<div className="flex-shrink-0 mt-1">
{getActivityIcon(activity)}
</div>
<div className="flex-1 min-w-0">
<h3 className="font-semibold text-sm mb-1">{activity.name}</h3>
<p className="text-xs opacity-80 mb-2">{activity.description}</p>
<div className="flex flex-col gap-1">
<div className="flex items-center space-x-3">
<div className="flex items-center space-x-1">
<Clock className="w-3 h-3" />
<span>{activity.estimatedTime}</span>
</div>
<div className="flex items-center space-x-1">
<div className={`
w-3 h-3 rounded-full shadow-sm
${activity.difficulty <= 2 ? 'bg-green-400' :
activity.difficulty <= 4 ? 'bg-yellow-400' : 'bg-red-400'}
`}></div>
<span>Level {activity.difficulty}</span>
</div>
</div>
{activity.prerequisites.length > 0 && (
<div className="text-xs text-gray-500">
Prerequisites: {activity.prerequisites.length}
</div>
)}
</div>
</div>
</div>
{learningMap.currentActivity === activity.id && (
<div className="absolute -left-2 top-1/2 transform -translate-y-1/2">
<div className="w-1 h-8 bg-gradient-to-b from-blue-400 to-blue-600 rounded-full shadow-sm"></div>
</div>
)}
</div>
{index < learningMap.activities.length - 1 && (
<div className="flex justify-center py-2">
<div className="flex flex-col items-center">
<div className="w-px h-4 bg-gray-300"></div>
<ArrowDown className="w-4 h-4 text-gray-400" />
<div className="w-px h-4 bg-gray-300"></div>
</div>
</div>
)}
</div>
))}
</div>
{/* Legend */}
<div className="mt-6 p-3 bg-gradient-to-r from-gray-50 to-gray-100 rounded-lg border">
<h4 className="text-xs font-semibold text-gray-700 mb-2">Legend</h4>
<div className="grid grid-cols-3 gap-2 text-xs">
<div className="flex items-center space-x-1">
<CheckCircle className="w-3 h-3 text-green-500" />
<span className="text-gray-600">Completed</span>
</div>
<div className="flex items-center space-x-1">
<Target className="w-3 h-3 text-blue-500" />
<span className="text-gray-600">Available</span>
</div>
<div className="flex items-center space-x-1">
<Lock className="w-3 h-3 text-gray-400" />
<span className="text-gray-600">Locked</span>
</div>
</div>
</div>
</div>
);
}; |