wiki-project / src /components /StudyPlansSection.tsx
Nagi15's picture
Add codebase
fcb5a67
import React, { useState, useCallback } from 'react';
import { BookOpen, Clock, Target, Plus, CheckCircle, Circle, Star, TrendingUp, Play, ExternalLink, Loader2, Eye, ArrowRight, AlertCircle } from 'lucide-react';
import { StudyPlan, StudyTopic } from '../types';
import { WikimediaAPI } from '../utils/wikimedia-api';
interface StudyPlansSectionProps {
studyPlans: StudyPlan[];
onTopicComplete?: (planId: string, topicId: string) => void;
onTopicStart?: (planId: string, topicId: string) => void;
onPlanCreated?: (plan: StudyPlan) => void;
onViewArticle?: (title: string, project: string, content: string) => void;
}
interface CreatePlanFormProps {
onPlanCreated?: (plan: StudyPlan) => void;
onCancel: () => void;
}
// Move the form component outside to prevent recreation
const CreatePlanForm: React.FC<CreatePlanFormProps> = ({ onPlanCreated, onCancel }) => {
const [title, setTitle] = useState('');
const [description, setDescription] = useState('');
const [difficulty, setDifficulty] = useState<'beginner' | 'intermediate' | 'advanced'>('beginner');
const [estimatedTime, setEstimatedTime] = useState('');
const [creatingPlan, setCreatingPlan] = useState(false);
const [generationErrorMessage, setGenerationErrorMessage] = useState<string>('');
const clearForm = () => {
setTitle('');
setDescription('');
setDifficulty('beginner');
setEstimatedTime('');
setGenerationErrorMessage('');
};
const handleCreatePlan = async (e: React.FormEvent) => {
e.preventDefault();
if (!title.trim()) return;
setCreatingPlan(true);
setGenerationErrorMessage('');
try {
// Use AI to generate a real study plan
const generatedPlan = await WikimediaAPI.generateStudyPlan(title, difficulty);
// Override with user's custom details
const customPlan: StudyPlan = {
...generatedPlan,
title: title,
description: description || generatedPlan.description,
estimatedTime: estimatedTime || generatedPlan.estimatedTime
};
if (onPlanCreated) {
onPlanCreated(customPlan);
}
onCancel();
clearForm();
} catch (error) {
console.error('Failed to create study plan:', error);
// Check if it's the "No content found" error
if (error instanceof Error && error.message === 'No content found for this topic') {
setGenerationErrorMessage(
'We couldn\'t find enough content for this topic. Try using a more general topic like "Physics", "History", "Biology", or "Computer Science".'
);
} else {
setGenerationErrorMessage(
'Failed to generate AI study plan. We\'ll create a basic plan for you instead.'
);
// Create a basic plan if API fails
const basicPlan: StudyPlan = {
id: `custom-${Date.now()}`,
title: title,
description: description || `Study plan for ${title}`,
difficulty: difficulty,
estimatedTime: estimatedTime || '4 weeks',
created: new Date().toISOString(),
topics: [
{
id: `${Date.now()}-1`,
title: `Introduction to ${title}`,
description: 'Getting started with the basics',
content: 'Introductory content will be loaded from Wikimedia sources.',
completed: false,
estimatedTime: '2 hours',
resources: []
}
]
};
if (onPlanCreated) {
onPlanCreated(basicPlan);
}
onCancel();
clearForm();
}
} finally {
setCreatingPlan(false);
}
};
return (
<div className="bg-white rounded-2xl p-6 border border-gray-200 shadow-sm">
<h3 className="text-lg font-semibold text-gray-900 mb-4">Create New Study Plan</h3>
{generationErrorMessage && (
<div className="mb-4 p-4 bg-red-50 border border-red-200 rounded-xl flex items-start space-x-3">
<AlertCircle className="w-5 h-5 text-red-600 flex-shrink-0 mt-0.5" />
<div>
<p className="text-red-800 font-medium">Unable to Generate Study Plan</p>
<p className="text-red-700 text-sm mt-1">{generationErrorMessage}</p>
</div>
</div>
)}
<form onSubmit={handleCreatePlan} className="space-y-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">Plan Title</label>
<input
type="text"
value={title}
onChange={(e) => {
setTitle(e.target.value);
setGenerationErrorMessage(''); // Clear error when user types
}}
placeholder="e.g., Introduction to Quantum Physics"
className="w-full px-4 py-3 border border-gray-300 rounded-xl focus:ring-2 focus:ring-primary-500 focus:border-transparent"
required
disabled={creatingPlan}
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">Description</label>
<textarea
value={description}
onChange={(e) => {
setDescription(e.target.value);
setGenerationErrorMessage(''); // Clear error when user types
}}
placeholder="Brief description of what this study plan covers..."
rows={3}
className="w-full px-4 py-3 border border-gray-300 rounded-xl focus:ring-2 focus:ring-primary-500 focus:border-transparent"
disabled={creatingPlan}
/>
</div>
<div className="grid grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">Difficulty</label>
<select
value={difficulty}
onChange={(e) => setDifficulty(e.target.value as 'beginner' | 'intermediate' | 'advanced')}
className="w-full px-4 py-3 border border-gray-300 rounded-xl focus:ring-2 focus:ring-primary-500 focus:border-transparent"
disabled={creatingPlan}
>
<option value="beginner">Beginner</option>
<option value="intermediate">Intermediate</option>
<option value="advanced">Advanced</option>
</select>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">Estimated Time</label>
<input
type="text"
value={estimatedTime}
onChange={(e) => setEstimatedTime(e.target.value)}
placeholder="e.g., 4 weeks"
className="w-full px-4 py-3 border border-gray-300 rounded-xl focus:ring-2 focus:ring-primary-500 focus:border-transparent"
disabled={creatingPlan}
/>
</div>
</div>
<div className="flex space-x-3 pt-4">
<button
type="button"
onClick={() => {
onCancel();
clearForm();
}}
className="flex-1 px-4 py-3 border border-gray-300 rounded-xl text-gray-700 hover:bg-gray-50 transition-colors"
disabled={creatingPlan}
>
Cancel
</button>
<button
type="submit"
disabled={creatingPlan || !title.trim()}
className="flex-1 flex items-center justify-center space-x-2 px-4 py-3 bg-primary-600 text-white rounded-xl hover:bg-primary-700 transition-colors disabled:opacity-50"
>
{creatingPlan ? (
<>
<Loader2 className="w-4 h-4 animate-spin" />
<span>Creating Plan...</span>
</>
) : (
<span>Create Plan</span>
)}
</button>
</div>
</form>
</div>
);
};
const StudyPlansSection: React.FC<StudyPlansSectionProps> = ({
studyPlans,
onTopicComplete,
onTopicStart,
onPlanCreated,
onViewArticle
}) => {
const [selectedPlan, setSelectedPlan] = useState<StudyPlan | null>(null);
const [showCreateForm, setShowCreateForm] = useState(false);
const [loadingResource, setLoadingResource] = useState<string | null>(null);
const [startingTopic, setStartingTopic] = useState<string | null>(null);
const getDifficultyColor = (difficulty: string) => {
switch (difficulty) {
case 'beginner': return 'bg-emerald-100 text-emerald-800 border-emerald-200';
case 'intermediate': return 'bg-amber-100 text-amber-800 border-amber-200';
case 'advanced': return 'bg-red-100 text-red-800 border-red-200';
default: return 'bg-gray-100 text-gray-800 border-gray-200';
}
};
const getCompletionPercentage = (topics: StudyTopic[]) => {
if (topics.length === 0) return 0;
const completed = topics.filter(t => t.completed).length;
return Math.round((completed / topics.length) * 100);
};
const handleTopicAction = async (planId: string, topicId: string, action: 'start' | 'complete') => {
if (action === 'start') {
setStartingTopic(topicId);
// Find the topic and load its content
const plan = studyPlans.find(p => p.id === planId);
const topic = plan?.topics.find(t => t.id === topicId);
if (topic && topic.resources.length > 0 && onViewArticle) {
try {
const resource = topic.resources[0];
const content = await WikimediaAPI.getPageContent(resource.title, resource.project);
onViewArticle(resource.title, resource.project, content);
} catch (error) {
console.error('Failed to load topic content:', error);
}
}
if (onTopicStart) {
onTopicStart(planId, topicId);
}
setStartingTopic(null);
} else if (action === 'complete' && onTopicComplete) {
// Immediately update the completion status
onTopicComplete(planId, topicId);
}
};
const handleViewResource = async (resource: any) => {
setLoadingResource(resource.url);
try {
const content = await WikimediaAPI.getPageContent(resource.title, resource.project);
if (onViewArticle) {
onViewArticle(resource.title, resource.project, content);
}
} catch (error) {
console.error('Failed to load resource content:', error);
} finally {
setLoadingResource(null);
}
};
const getNextTopic = (plan: StudyPlan) => {
const nextTopic = plan.topics.find(topic => !topic.completed);
return nextTopic;
};
if (selectedPlan) {
const nextTopic = getNextTopic(selectedPlan);
return (
<div className="max-w-5xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
<div className="mb-8">
<button
onClick={() => setSelectedPlan(null)}
className="flex items-center space-x-2 text-primary-600 hover:text-primary-700 font-medium mb-6 transition-colors"
>
<ArrowRight className="w-4 h-4 rotate-180" />
<span>Back to Study Plans</span>
</button>
<div className="bg-white rounded-2xl p-8 border border-gray-200 shadow-sm">
<div className="flex items-start justify-between mb-6">
<div className="flex-1">
<div className="flex items-center space-x-3 mb-3">
<h1 className="text-3xl font-bold text-gray-900">{selectedPlan.title}</h1>
<span className="px-3 py-1 bg-gradient-to-r from-primary-100 to-secondary-100 text-primary-700 text-sm rounded-full font-medium border border-primary-200">
AI Generated
</span>
</div>
<p className="text-gray-600 mb-6 text-lg leading-relaxed">{selectedPlan.description}</p>
<div className="flex items-center space-x-6">
<span className={`px-4 py-2 rounded-full text-sm font-medium border ${getDifficultyColor(selectedPlan.difficulty)}`}>
{selectedPlan.difficulty.charAt(0).toUpperCase() + selectedPlan.difficulty.slice(1)}
</span>
<div className="flex items-center text-gray-600">
<Clock className="w-5 h-5 mr-2" />
<span className="font-medium">{selectedPlan.estimatedTime}</span>
</div>
<div className="flex items-center text-gray-600">
<Target className="w-5 h-5 mr-2" />
<span className="font-medium">{getCompletionPercentage(selectedPlan.topics)}% Complete</span>
</div>
</div>
</div>
<div className="ml-6">
<div className="w-20 h-20 bg-gradient-to-br from-primary-100 to-secondary-100 rounded-2xl flex items-center justify-center border border-primary-200">
<TrendingUp className="w-10 h-10 text-primary-600" />
</div>
</div>
</div>
<div className="w-full bg-gray-200 rounded-full h-3 mb-6">
<div
className="bg-gradient-to-r from-primary-500 to-secondary-500 h-3 rounded-full transition-all duration-500"
style={{ width: `${getCompletionPercentage(selectedPlan.topics)}%` }}
/>
</div>
{/* What's Next Section */}
{nextTopic && (
<div className="bg-gradient-to-r from-primary-50 to-secondary-50 rounded-2xl p-6 border border-primary-200">
<h3 className="font-semibold text-primary-900 mb-3 text-lg">🎯 What's Next?</h3>
<div className="flex items-center justify-between">
<div className="flex-1">
<p className="text-primary-800 font-semibold text-lg">{nextTopic.title}</p>
<p className="text-primary-600 mt-1">{nextTopic.estimatedTime} • {nextTopic.resources.length} resources available</p>
</div>
<button
onClick={() => handleTopicAction(selectedPlan.id, nextTopic.id, 'start')}
disabled={startingTopic === nextTopic.id}
className="flex items-center space-x-2 px-6 py-3 bg-primary-600 text-white rounded-xl hover:bg-primary-700 transition-colors shadow-md hover:shadow-lg disabled:opacity-50"
>
{startingTopic === nextTopic.id ? (
<>
<Loader2 className="w-5 h-5 animate-spin" />
<span className="font-medium">Starting...</span>
</>
) : (
<>
<Play className="w-5 h-5" />
<span className="font-medium">Start Now</span>
</>
)}
</button>
</div>
</div>
)}
</div>
</div>
<div className="space-y-6">
{selectedPlan.topics.map((topic, index) => (
<div
key={topic.id}
className="bg-white rounded-2xl p-6 border border-gray-200 shadow-sm hover:shadow-md transition-all duration-200"
>
<div className="flex items-start space-x-4">
<div className="flex-shrink-0 mt-1">
{topic.completed ? (
<CheckCircle className="w-7 h-7 text-emerald-600" />
) : (
<Circle className="w-7 h-7 text-gray-400" />
)}
</div>
<div className="flex-1">
<div className="flex items-start justify-between">
<div className="flex-1">
<h3 className="text-xl font-semibold text-gray-900 mb-2">
{index + 1}. {topic.title}
</h3>
<p className="text-gray-600 mb-4 leading-relaxed">{topic.description}</p>
<div className="flex items-center space-x-6 text-sm text-gray-500 mb-4">
<div className="flex items-center">
<Clock className="w-4 h-4 mr-1" />
<span>{topic.estimatedTime}</span>
</div>
<div className="flex items-center">
<BookOpen className="w-4 h-4 mr-1" />
<span>{topic.resources.length} resources</span>
</div>
</div>
{topic.resources.length > 0 && (
<div className="space-y-3">
<h4 className="font-medium text-gray-900">📚 Resources:</h4>
<div className="flex flex-wrap gap-3">
{topic.resources.map((resource, resourceIndex) => (
<div key={resourceIndex} className="flex items-center space-x-2">
<button
onClick={() => handleViewResource(resource)}
disabled={loadingResource === resource.url}
className="inline-flex items-center px-4 py-2 bg-primary-100 text-primary-700 rounded-xl text-sm hover:bg-primary-200 transition-colors disabled:opacity-50 border border-primary-200"
>
{loadingResource === resource.url ? (
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
) : (
<Eye className="w-4 h-4 mr-2" />
)}
<span className="font-medium">{resource.title}</span>
</button>
<a
href={resource.url}
target="_blank"
rel="noopener noreferrer"
className="p-2 text-gray-400 hover:text-primary-600 transition-colors rounded-lg hover:bg-gray-100"
>
<ExternalLink className="w-4 h-4" />
</a>
</div>
))}
</div>
</div>
)}
</div>
<div className="ml-6 flex flex-col space-y-3">
{!topic.completed ? (
<>
<button
onClick={() => handleTopicAction(selectedPlan.id, topic.id, 'start')}
disabled={startingTopic === topic.id}
className="flex items-center space-x-2 px-5 py-3 bg-primary-600 text-white rounded-xl hover:bg-primary-700 transition-colors shadow-md hover:shadow-lg disabled:opacity-50"
>
{startingTopic === topic.id ? (
<>
<Loader2 className="w-4 h-4 animate-spin" />
<span className="font-medium">Starting...</span>
</>
) : (
<>
<Play className="w-4 h-4" />
<span className="font-medium">Start</span>
</>
)}
</button>
<button
onClick={() => handleTopicAction(selectedPlan.id, topic.id, 'complete')}
className="flex items-center space-x-2 px-5 py-3 border-2 border-emerald-600 text-emerald-600 rounded-xl hover:bg-emerald-50 transition-colors"
>
<CheckCircle className="w-4 h-4" />
<span className="font-medium">Complete</span>
</button>
</>
) : (
<button className="flex items-center space-x-2 px-5 py-3 bg-emerald-600 text-white rounded-xl shadow-md">
<CheckCircle className="w-4 h-4" />
<span className="font-medium">Completed</span>
</button>
)}
</div>
</div>
</div>
</div>
</div>
))}
</div>
</div>
);
}
return (
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
<div className="mb-8">
<div className="flex items-center justify-between">
<div>
<h1 className="text-4xl font-bold text-gray-900 mb-3">Study Plans</h1>
<p className="text-gray-600 text-lg">Structured learning paths powered by Wikimedia content</p>
{studyPlans.length > 0 && (
<p className="text-primary-600 mt-2 font-medium">
{studyPlans.length} AI-generated plan{studyPlans.length > 1 ? 's' : ''} available
</p>
)}
</div>
<button
onClick={() => setShowCreateForm(true)}
className="flex items-center space-x-2 px-6 py-3 bg-primary-600 text-white rounded-xl hover:bg-primary-700 transition-colors shadow-md hover:shadow-lg"
>
<Plus className="w-5 h-5" />
<span className="font-medium">Create Plan</span>
</button>
</div>
</div>
{showCreateForm && (
<div className="mb-8">
<CreatePlanForm
onPlanCreated={onPlanCreated}
onCancel={() => setShowCreateForm(false)}
/>
</div>
)}
{studyPlans.length === 0 ? (
<div className="bg-white rounded-2xl p-12 text-center border border-gray-200 shadow-sm">
<BookOpen className="w-20 h-20 text-gray-300 mx-auto mb-6" />
<h3 className="text-2xl font-medium text-gray-900 mb-3">No Study Plans Yet</h3>
<p className="text-gray-600 mb-6 text-lg">
Create your first study plan or use the AI Generator to get started with personalized learning paths.
</p>
<button
onClick={() => setShowCreateForm(true)}
className="px-6 py-3 bg-primary-600 text-white rounded-xl hover:bg-primary-700 transition-colors font-medium"
>
Create Your First Plan
</button>
</div>
) : (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8">
{studyPlans.map((plan) => {
const nextTopic = getNextTopic(plan);
return (
<div
key={plan.id}
onClick={() => setSelectedPlan(plan)}
className="bg-white rounded-2xl p-6 border border-gray-200 shadow-sm hover:shadow-lg transition-all duration-300 cursor-pointer hover:border-primary-200 group"
>
<div className="flex items-start justify-between mb-4">
<div className="flex-1">
<div className="flex items-center space-x-2 mb-3">
<h3 className="text-xl font-semibold text-gray-900 group-hover:text-primary-600 transition-colors">{plan.title}</h3>
<span className="px-2 py-1 bg-primary-100 text-primary-700 text-xs rounded-full font-medium border border-primary-200">
AI
</span>
</div>
<p className="text-gray-600 text-sm mb-4 leading-relaxed">{plan.description}</p>
<div className="flex items-center space-x-4 mb-4">
<span className={`px-3 py-1 rounded-full text-xs font-medium border ${getDifficultyColor(plan.difficulty)}`}>
{plan.difficulty.charAt(0).toUpperCase() + plan.difficulty.slice(1)}
</span>
<div className="flex items-center text-xs text-gray-600">
<Clock className="w-3 h-3 mr-1" />
<span>{plan.estimatedTime}</span>
</div>
</div>
<div className="mb-4">
<div className="flex items-center justify-between text-sm text-gray-600 mb-2">
<span>Progress</span>
<span className="font-medium">{getCompletionPercentage(plan.topics)}%</span>
</div>
<div className="w-full bg-gray-200 rounded-full h-2">
<div
className="bg-gradient-to-r from-primary-500 to-secondary-500 h-2 rounded-full transition-all duration-300"
style={{ width: `${getCompletionPercentage(plan.topics)}%` }}
/>
</div>
</div>
<div className="flex items-center justify-between text-sm text-gray-600">
<div className="flex items-center">
<BookOpen className="w-4 h-4 mr-1" />
<span>{plan.topics.length} topics</span>
</div>
{nextTopic && (
<div className="text-primary-600 font-medium">
Next: {nextTopic.title.substring(0, 15)}...
</div>
)}
</div>
</div>
<div className="ml-4">
<div className="w-14 h-14 bg-gradient-to-br from-primary-100 to-secondary-100 rounded-xl flex items-center justify-center border border-primary-200 group-hover:scale-110 transition-transform">
<Star className="w-7 h-7 text-primary-600" />
</div>
</div>
</div>
</div>
);
})}
</div>
)}
</div>
);
};
export default StudyPlansSection;