Spaces:
Runtime error
Runtime error
| 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; |